@milaboratories/pl-tree 1.10.6 → 1.11.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/index.d.ts +2 -2
- package/dist/sync.cjs +112 -19
- package/dist/sync.cjs.map +1 -1
- package/dist/sync.d.ts +23 -10
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +112 -19
- package/dist/sync.js.map +1 -1
- package/dist/synchronized_tree.cjs +32 -8
- package/dist/synchronized_tree.cjs.map +1 -1
- package/dist/synchronized_tree.d.ts +11 -7
- package/dist/synchronized_tree.d.ts.map +1 -1
- package/dist/synchronized_tree.js +32 -8
- package/dist/synchronized_tree.js.map +1 -1
- package/package.json +5 -5
- package/src/sync.test.ts +443 -2
- package/src/sync.ts +202 -29
- package/src/synchronized_tree.ts +79 -22
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { ValueAndError, mapValueAndError, mapValueAndErrorIfDefined } from "./va
|
|
|
3
3
|
import { ValueOrError } from "./value_or_error.js";
|
|
4
4
|
import { PlError, PlTreeEntry, PlTreeEntryAccessor, PlTreeNodeAccessor, ResourceInfo, TreeAccessorData, TreeAccessorInstanceData, isPlTreeEntry, isPlTreeEntryAccessor, isPlTreeNodeAccessor, treeEntryToResourceInfo } from "./accessors.js";
|
|
5
5
|
import { ExtendedResourceData, PlTreeResource, PlTreeState, ResourceDataWithFinalState, TreeStateUpdateError } from "./state.js";
|
|
6
|
-
import { PruningFunction, TreeLoadingRequest, TreeLoadingStat, constructTreeLoadingRequest, formatTreeLoadingStat, initialTreeLoadingStat, loadTreeState } from "./sync.js";
|
|
6
|
+
import { PruningFunction, TraversalMode, TreeLoadingRequest, TreeLoadingStat, constructTreeLoadingRequest, formatTreeLoadingStat, initialTreeLoadingStat, loadTreeState } from "./sync.js";
|
|
7
7
|
import { InferSnapshot, ResourceSnapshot, ResourceSnapshotSchema, ResourceWithData, ResourceWithMetadata, makeResourceSnapshot, rsSchema, treeEntryToResourceWithData, treeEntryToResourceWithMetadata } from "./snapshot.js";
|
|
8
8
|
import { SynchronizedTreeOps, SynchronizedTreeState } from "./synchronized_tree.js";
|
|
9
9
|
import { ResourceStats, TreeDumpStats, treeDumpStats } from "./dump.js";
|
|
10
|
-
export { CommonFieldTraverseOps, CommonTraversalOps, ExtendedResourceData, FieldTraversalStep, GetFieldStep, InferSnapshot, PlError, PlTreeEntry, PlTreeEntryAccessor, PlTreeNodeAccessor, PlTreeResource, PlTreeState, PruningFunction, ResourceDataWithFinalState, ResourceInfo, ResourceSnapshot, ResourceSnapshotSchema, ResourceStats, ResourceTraversalOps, ResourceWithData, ResourceWithMetadata, SynchronizedTreeOps, SynchronizedTreeState, TreeAccessorData, TreeAccessorInstanceData, TreeDumpStats, TreeLoadingRequest, TreeLoadingStat, TreeStateUpdateError, ValueAndError, ValueOrError, constructTreeLoadingRequest, formatTreeLoadingStat, initialTreeLoadingStat, isPlTreeEntry, isPlTreeEntryAccessor, isPlTreeNodeAccessor, loadTreeState, makeResourceSnapshot, mapValueAndError, mapValueAndErrorIfDefined, rsSchema, treeDumpStats, treeEntryToResourceInfo, treeEntryToResourceWithData, treeEntryToResourceWithMetadata };
|
|
10
|
+
export { CommonFieldTraverseOps, CommonTraversalOps, ExtendedResourceData, FieldTraversalStep, GetFieldStep, InferSnapshot, PlError, PlTreeEntry, PlTreeEntryAccessor, PlTreeNodeAccessor, PlTreeResource, PlTreeState, PruningFunction, ResourceDataWithFinalState, ResourceInfo, ResourceSnapshot, ResourceSnapshotSchema, ResourceStats, ResourceTraversalOps, ResourceWithData, ResourceWithMetadata, SynchronizedTreeOps, SynchronizedTreeState, TraversalMode, TreeAccessorData, TreeAccessorInstanceData, TreeDumpStats, TreeLoadingRequest, TreeLoadingStat, TreeStateUpdateError, ValueAndError, ValueOrError, constructTreeLoadingRequest, formatTreeLoadingStat, initialTreeLoadingStat, isPlTreeEntry, isPlTreeEntryAccessor, isPlTreeNodeAccessor, loadTreeState, makeResourceSnapshot, mapValueAndError, mapValueAndErrorIfDefined, rsSchema, treeDumpStats, treeEntryToResourceInfo, treeEntryToResourceWithData, treeEntryToResourceWithMetadata };
|
package/dist/sync.cjs
CHANGED
|
@@ -4,9 +4,10 @@ let _milaboratories_ts_helpers = require("@milaboratories/ts-helpers");
|
|
|
4
4
|
let denque = require("denque");
|
|
5
5
|
denque = require_runtime.__toESM(denque);
|
|
6
6
|
//#region src/sync.ts
|
|
7
|
+
const CapabilityTreeFilter = "treeFilter:v1";
|
|
7
8
|
/** Given the current tree state, build the request object to pass to
|
|
8
9
|
* {@link loadTreeState} to load updated state. */
|
|
9
|
-
function constructTreeLoadingRequest(tree,
|
|
10
|
+
function constructTreeLoadingRequest(tree, options = {}) {
|
|
10
11
|
const seedResources = [];
|
|
11
12
|
const finalResources = /* @__PURE__ */ new Set();
|
|
12
13
|
tree.forEachResource((res) => {
|
|
@@ -17,7 +18,9 @@ function constructTreeLoadingRequest(tree, pruningFunction) {
|
|
|
17
18
|
return {
|
|
18
19
|
seedResources,
|
|
19
20
|
finalResources,
|
|
20
|
-
pruningFunction
|
|
21
|
+
pruningFunction: options.pruningFunction,
|
|
22
|
+
fieldFilter: options.fieldFilter,
|
|
23
|
+
traverseStopRules: options.traverseStopRules
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
26
|
function initialTreeLoadingStat() {
|
|
@@ -31,7 +34,9 @@ function initialTreeLoadingStat() {
|
|
|
31
34
|
retrievedKeyValueBytes: 0,
|
|
32
35
|
prunedFields: 0,
|
|
33
36
|
finalResourcesSkipped: 0,
|
|
34
|
-
millisSpent: 0
|
|
37
|
+
millisSpent: 0,
|
|
38
|
+
stopMarkersSkipped: 0,
|
|
39
|
+
stopMarkerFollowUpRoundTrips: 0
|
|
35
40
|
};
|
|
36
41
|
}
|
|
37
42
|
function formatTreeLoadingStat(stat) {
|
|
@@ -44,15 +49,23 @@ function formatTreeLoadingStat(stat) {
|
|
|
44
49
|
result += `Data Bytes: ${stat.retrievedResourceDataBytes}\n`;
|
|
45
50
|
result += `KV Bytes: ${stat.retrievedKeyValueBytes}\n`;
|
|
46
51
|
result += `Pruned fields: ${stat.prunedFields}\n`;
|
|
47
|
-
result += `Final resources skipped: ${stat.finalResourcesSkipped}`;
|
|
52
|
+
result += `Final resources skipped: ${stat.finalResourcesSkipped}\n`;
|
|
53
|
+
result += `Stop markers skipped: ${stat.stopMarkersSkipped}\n`;
|
|
54
|
+
result += `Stop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}`;
|
|
48
55
|
return result;
|
|
49
56
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
function supportsResourceTreeTraversal(capabilities = []) {
|
|
58
|
+
return capabilities.includes(CapabilityTreeFilter);
|
|
59
|
+
}
|
|
60
|
+
function collectStatsForResource(resource, stats) {
|
|
61
|
+
if (!stats) return;
|
|
62
|
+
stats.retrievedResources++;
|
|
63
|
+
stats.retrievedFields += resource.fields.length;
|
|
64
|
+
stats.retrievedKeyValues += resource.kv.length;
|
|
65
|
+
stats.retrievedResourceDataBytes += resource.data?.length ?? 0;
|
|
66
|
+
for (const kv of resource.kv) stats.retrievedKeyValueBytes += kv.value.length;
|
|
67
|
+
}
|
|
68
|
+
async function loadTreeStateViaBfs(tx, loadingRequest, stats) {
|
|
56
69
|
const { seedResources, finalResources, pruningFunction } = loadingRequest;
|
|
57
70
|
const limiter = new _milaboratories_ts_helpers.ConcurrencyLimitingExecutor(100);
|
|
58
71
|
const pending = new denque.default();
|
|
@@ -104,21 +117,101 @@ async function loadTreeState(tx, loadingRequest, stats) {
|
|
|
104
117
|
requestState(field.value);
|
|
105
118
|
requestState(field.error);
|
|
106
119
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
collectStatsForResource(nextResource, stats);
|
|
121
|
+
result.push(nextResource);
|
|
122
|
+
}
|
|
123
|
+
if (stats) stats.roundTrips += numberOfRoundTrips;
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
async function processResourceTreeStream(treeItems, finalResources, pruningFunction, stats) {
|
|
127
|
+
const result = [];
|
|
128
|
+
const followUpSeeds = [];
|
|
129
|
+
for await (const frame of treeItems) {
|
|
130
|
+
if (frame.frameKind === "stopMarker") {
|
|
131
|
+
if (finalResources.has(frame.id)) {
|
|
132
|
+
if (stats) stats.stopMarkersSkipped++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
followUpSeeds.push(frame.id);
|
|
136
|
+
continue;
|
|
113
137
|
}
|
|
138
|
+
if (finalResources.has(frame.id)) {
|
|
139
|
+
if (stats) stats.finalResourcesSkipped++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
let nextResource = {
|
|
143
|
+
id: frame.id,
|
|
144
|
+
type: frame.type,
|
|
145
|
+
kind: frame.kind,
|
|
146
|
+
data: frame.data,
|
|
147
|
+
resourceReady: frame.resourceReady,
|
|
148
|
+
error: frame.error,
|
|
149
|
+
originalResourceId: frame.originalResourceId,
|
|
150
|
+
final: frame.final || frame.traverseWasStopped,
|
|
151
|
+
inputsLocked: frame.inputsLocked,
|
|
152
|
+
outputsLocked: frame.outputsLocked,
|
|
153
|
+
fields: frame.fields,
|
|
154
|
+
kv: frame.kv
|
|
155
|
+
};
|
|
156
|
+
const rawFields = frame.traverseWasStopped ? [] : nextResource.fields;
|
|
157
|
+
const resolvedFields = pruningFunction !== void 0 ? pruningFunction({
|
|
158
|
+
...nextResource,
|
|
159
|
+
fields: rawFields
|
|
160
|
+
}) : rawFields;
|
|
161
|
+
if (stats) stats.prunedFields += nextResource.fields.length - resolvedFields.length;
|
|
162
|
+
nextResource = {
|
|
163
|
+
...nextResource,
|
|
164
|
+
fields: resolvedFields
|
|
165
|
+
};
|
|
166
|
+
collectStatsForResource(nextResource, stats);
|
|
114
167
|
result.push(nextResource);
|
|
115
168
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
169
|
+
return {
|
|
170
|
+
result,
|
|
171
|
+
followUpSeeds
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger) {
|
|
175
|
+
const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } = loadingRequest;
|
|
176
|
+
const { result, followUpSeeds } = await processResourceTreeStream(tx.resourceTree(seedResources, {
|
|
177
|
+
includeKv: true,
|
|
178
|
+
fieldFilter,
|
|
179
|
+
traverseStopRules
|
|
180
|
+
}), finalResources, pruningFunction, stats);
|
|
181
|
+
if (stats) stats.roundTrips++;
|
|
182
|
+
if (followUpSeeds.length > 0) {
|
|
183
|
+
const { result: followUpResult } = await processResourceTreeStream(tx.resourceTree(followUpSeeds, {
|
|
184
|
+
includeKv: true,
|
|
185
|
+
fieldFilter
|
|
186
|
+
}), finalResources, pruningFunction, stats);
|
|
187
|
+
result.push(...followUpResult);
|
|
188
|
+
if (stats) {
|
|
189
|
+
logger?.info?.(`loadTreeStateViaResourceTree: follow-up request for ${followUpSeeds.length} stop-marker seeds: ${JSON.stringify(followUpSeeds)}`);
|
|
190
|
+
stats.roundTrips++;
|
|
191
|
+
stats.stopMarkerFollowUpRoundTrips++;
|
|
192
|
+
}
|
|
119
193
|
}
|
|
120
194
|
return result;
|
|
121
195
|
}
|
|
196
|
+
/** Given the transaction (preferably read-only) and loading request, executes
|
|
197
|
+
* the tree traversal algorithm, and collects fresh states of resources
|
|
198
|
+
* to update the tree state. */
|
|
199
|
+
async function loadTreeState(tx, loadingRequest, stats, capabilities = [], mode = "auto", logger) {
|
|
200
|
+
const startTimestamp = Date.now();
|
|
201
|
+
if (stats) stats.requests++;
|
|
202
|
+
try {
|
|
203
|
+
const wantsStreaming = mode === "backend-streaming" || mode === "auto" && supportsResourceTreeTraversal(capabilities);
|
|
204
|
+
if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {
|
|
205
|
+
const msg = "traversalMode=backend-streaming but backend lacks treeFilter:v1 capability; falling back to BFS";
|
|
206
|
+
if (logger) logger.warn(msg);
|
|
207
|
+
else console.warn(msg);
|
|
208
|
+
return await loadTreeStateViaBfs(tx, loadingRequest, stats);
|
|
209
|
+
}
|
|
210
|
+
return wantsStreaming ? await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger) : await loadTreeStateViaBfs(tx, loadingRequest, stats);
|
|
211
|
+
} finally {
|
|
212
|
+
if (stats) stats.millisSpent += Date.now() - startTimestamp;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
122
215
|
//#endregion
|
|
123
216
|
exports.constructTreeLoadingRequest = constructTreeLoadingRequest;
|
|
124
217
|
exports.formatTreeLoadingStat = formatTreeLoadingStat;
|
package/dist/sync.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.cjs","names":["ConcurrencyLimitingExecutor","Denque"],"sources":["../src/sync.ts"],"sourcesContent":["import type {\n FieldData,\n OptionalSignedResourceId,\n PlTransaction,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport Denque from \"denque\";\nimport { isNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData, PlTreeState } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\n\n/** Applied to list of fields in resource data. */\nexport type PruningFunction = (resource: ExtendedResourceData) => FieldData[];\n\nexport interface TreeLoadingRequest {\n /** Resource to prime the traversal algorithm. It is ok, if some of them\n * doesn't exist anymore. Should not contain elements from final resource\n * set. */\n readonly seedResources: SignedResourceId[];\n\n /** Resource ids for which state is already known and not expected to change.\n * Algorithm will not continue traversal over those ids, and states will not\n * be retrieved for them. */\n readonly finalResources: Set<SignedResourceId>;\n\n /** This function is applied to each resource data field list, before\n * using it continue traversal. This modification also is applied to\n * output data to make result self-consistent in terms that it will contain\n * all referenced resources, this is required to be able to pass it to tree\n * to update the state. */\n readonly pruningFunction?: PruningFunction;\n}\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n pruningFunction?: PruningFunction,\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n // if tree is empty, seeding tree reconstruction from the specified root\n if (seedResources.length === 0 && finalResources.size === 0) seedResources.push(tree.root);\n\n return { seedResources, finalResources, pruningFunction };\n}\n\nexport type TreeLoadingStat = {\n requests: number;\n roundTrips: number;\n retrievedResources: number;\n retrievedFields: number;\n retrievedKeyValues: number;\n retrievedResourceDataBytes: number;\n retrievedKeyValueBytes: number;\n prunedFields: number;\n finalResourcesSkipped: number;\n millisSpent: number;\n};\n\nexport function initialTreeLoadingStat(): TreeLoadingStat {\n return {\n requests: 0,\n roundTrips: 0,\n retrievedResources: 0,\n retrievedFields: 0,\n retrievedKeyValues: 0,\n retrievedResourceDataBytes: 0,\n retrievedKeyValueBytes: 0,\n prunedFields: 0,\n finalResourcesSkipped: 0,\n millisSpent: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n let result = `Requests: ${stat.requests}\\n`;\n result += `Total time: ${msToHumanReadable(stat.millisSpent)}\\n`;\n result += `Round-trips: ${stat.roundTrips}\\n`;\n result += `Resources: ${stat.retrievedResources}\\n`;\n result += `Fields: ${stat.retrievedFields}\\n`;\n result += `KV: ${stat.retrievedKeyValues}\\n`;\n result += `Data Bytes: ${stat.retrievedResourceDataBytes}\\n`;\n result += `KV Bytes: ${stat.retrievedKeyValueBytes}\\n`;\n result += `Pruned fields: ${stat.prunedFields}\\n`;\n result += `Final resources skipped: ${stat.finalResourcesSkipped}`;\n return result;\n}\n\n/** Given the transaction (preferably read-only) and loading request, executes\n * the tree traversal algorithm, and collects fresh states of resources\n * to update the tree state. */\nexport async function loadTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n): Promise<ExtendedResourceData[]> {\n // saving start timestamp to add time spent in this function to the stats at the end of the method\n const startTimestamp = Date.now();\n\n // counting the request\n if (stats) stats.requests++;\n\n const { seedResources, finalResources, pruningFunction } = loadingRequest;\n\n // Limits the number of concurrent gRPC fetches to bound peak memory\n // from in-flight request/response buffers.\n const limiter = new ConcurrencyLimitingExecutor(100);\n\n // Promises of resource states, in the order they were requested.\n const pending = new Denque<Promise<ExtendedResourceData | undefined>>();\n\n // vars to calculate number of roundtrips for stats\n let roundTripToggle: boolean = true;\n let numberOfRoundTrips = 0;\n\n // tracking resources we already requested or queued\n const requested = new Set<SignedResourceId>();\n\n /** Mark a resource for fetching. Deduplicates and respects final-resource set. */\n const requestState = (rid: OptionalSignedResourceId) => {\n if (isNullSignedResourceId(rid) || requested.has(rid)) return;\n\n if (finalResources.has(rid)) {\n if (stats) stats.finalResourcesSkipped++;\n return;\n }\n\n requested.add(rid);\n\n pending.push(\n limiter.run(async () => {\n const resourceData = tx.getResourceDataIfExists(rid, true);\n const kvData = tx.listKeyValuesIfResourceExists(rid);\n\n // counting round-trip (begin)\n const addRT = roundTripToggle;\n if (roundTripToggle) roundTripToggle = false;\n\n const [resource, kv] = await Promise.all([resourceData, kvData]);\n\n // counting round-trip, actually incrementing counter and returning toggle back,\n // so the next request can acquire it\n if (addRT) {\n numberOfRoundTrips++;\n roundTripToggle = true;\n }\n\n if (resource === undefined) return undefined;\n if (kv === undefined) throw new Error(\"Inconsistent replies\");\n\n return { ...resource, kv };\n }),\n );\n };\n\n // sending seed requests\n seedResources.forEach((rid) => requestState(rid));\n\n const result: ExtendedResourceData[] = [];\n let nextPromise: Promise<ExtendedResourceData | undefined> | undefined;\n while ((nextPromise = pending.shift()) !== undefined) {\n // at this point we pause and wait for the next requested resource state to arrive\n let nextResource = await nextPromise;\n if (nextResource === undefined)\n // ignoring resources that were not found (this may happen for seed resource ids)\n continue;\n\n if (pruningFunction !== undefined) {\n // apply field pruning, if requested\n const fieldsAfterPruning = pruningFunction(nextResource);\n // collecting stats\n if (stats) stats.prunedFields += nextResource.fields.length - fieldsAfterPruning.length;\n nextResource = { ...nextResource, fields: fieldsAfterPruning };\n }\n\n // continue traversal over the referenced resources\n requestState(nextResource.error);\n for (const field of nextResource.fields) {\n requestState(field.value);\n requestState(field.error);\n }\n\n // collecting stats\n if (stats) {\n stats.retrievedResources++;\n stats.retrievedFields += nextResource.fields.length;\n stats.retrievedKeyValues += nextResource.kv.length;\n stats.retrievedResourceDataBytes += nextResource.data?.length ?? 0;\n for (const kv of nextResource.kv) stats.retrievedKeyValueBytes += kv.value.length;\n }\n\n // aggregating the state\n result.push(nextResource);\n }\n\n // adding the time we spent in this method to stats\n if (stats) {\n stats.millisSpent += Date.now() - startTimestamp;\n stats.roundTrips += numberOfRoundTrips;\n }\n\n return result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sync.cjs","names":["ConcurrencyLimitingExecutor","Denque"],"sources":["../src/sync.ts"],"sourcesContent":["import type {\n FieldData,\n Filter,\n OptionalSignedResourceId,\n PlTransaction,\n ResourceTreeFrame,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport Denque from \"denque\";\nimport { isNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData, PlTreeState } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\n\n/** Applied to list of fields in resource data. */\nexport type PruningFunction = (resource: ExtendedResourceData) => FieldData[];\n\nexport interface TreeLoadingRequest {\n /** Resource to prime the traversal algorithm. It is ok, if some of them\n * doesn't exist anymore. Should not contain elements from final resource\n * set. */\n readonly seedResources: SignedResourceId[];\n\n /** Resource ids for which state is already known and not expected to change.\n * Algorithm will not continue traversal over those ids, and states will not\n * be retrieved for them. */\n readonly finalResources: Set<SignedResourceId>;\n\n /** Applied to each resource field list in fallback BFS mode and to streamed results. */\n readonly pruningFunction?: PruningFunction;\n\n /** ResourceTree field filter passed to the backend when supported. */\n readonly fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules passed to the backend when supported. */\n readonly traverseStopRules?: Filter;\n}\n\nconst CapabilityTreeFilter = \"treeFilter:v1\";\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use backend streaming when the backend advertises `treeFilter:v1`,\n * fall back to client-side BFS otherwise.\n * - `\"client-bfs\"`: always use client-side BFS, even on capable backends.\n * - `\"backend-streaming\"`: always prefer backend streaming; if the capability is absent,\n * logs a warning and falls back to BFS (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\";\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<TreeLoadingRequest, \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\"> = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n // if tree is empty, seeding tree reconstruction from the specified root\n if (seedResources.length === 0 && finalResources.size === 0) seedResources.push(tree.root);\n\n return {\n seedResources,\n finalResources,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n };\n}\n\nexport type TreeLoadingStat = {\n requests: number;\n roundTrips: number;\n retrievedResources: number;\n retrievedFields: number;\n retrievedKeyValues: number;\n retrievedResourceDataBytes: number;\n retrievedKeyValueBytes: number;\n prunedFields: number;\n finalResourcesSkipped: number;\n millisSpent: number;\n /** Stop-marker frames whose id was already final locally and were skipped. */\n stopMarkersSkipped: number;\n /** Number of follow-up resourceTree() calls issued to resolve unknown stop markers. */\n stopMarkerFollowUpRoundTrips: number;\n};\n\nexport function initialTreeLoadingStat(): TreeLoadingStat {\n return {\n requests: 0,\n roundTrips: 0,\n retrievedResources: 0,\n retrievedFields: 0,\n retrievedKeyValues: 0,\n retrievedResourceDataBytes: 0,\n retrievedKeyValueBytes: 0,\n prunedFields: 0,\n finalResourcesSkipped: 0,\n millisSpent: 0,\n stopMarkersSkipped: 0,\n stopMarkerFollowUpRoundTrips: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n let result = `Requests: ${stat.requests}\\n`;\n result += `Total time: ${msToHumanReadable(stat.millisSpent)}\\n`;\n result += `Round-trips: ${stat.roundTrips}\\n`;\n result += `Resources: ${stat.retrievedResources}\\n`;\n result += `Fields: ${stat.retrievedFields}\\n`;\n result += `KV: ${stat.retrievedKeyValues}\\n`;\n result += `Data Bytes: ${stat.retrievedResourceDataBytes}\\n`;\n result += `KV Bytes: ${stat.retrievedKeyValueBytes}\\n`;\n result += `Pruned fields: ${stat.prunedFields}\\n`;\n result += `Final resources skipped: ${stat.finalResourcesSkipped}\\n`;\n result += `Stop markers skipped: ${stat.stopMarkersSkipped}\\n`;\n result += `Stop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}`;\n return result;\n}\n\nfunction supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return capabilities.includes(CapabilityTreeFilter);\n}\n\nfunction collectStatsForResource(resource: ExtendedResourceData, stats?: TreeLoadingStat) {\n if (!stats) return;\n stats.retrievedResources++;\n stats.retrievedFields += resource.fields.length;\n stats.retrievedKeyValues += resource.kv.length;\n stats.retrievedResourceDataBytes += resource.data?.length ?? 0;\n for (const kv of resource.kv) stats.retrievedKeyValueBytes += kv.value.length;\n}\n\nasync function loadTreeStateViaBfs(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction } = loadingRequest;\n\n // Limits the number of concurrent gRPC fetches to bound peak memory\n // from in-flight request/response buffers.\n const limiter = new ConcurrencyLimitingExecutor(100);\n\n // Promises of resource states, in the order they were requested.\n const pending = new Denque<Promise<ExtendedResourceData | undefined>>();\n\n // vars to calculate number of roundtrips for stats\n let roundTripToggle: boolean = true;\n let numberOfRoundTrips = 0;\n\n // tracking resources we already requested or queued\n const requested = new Set<SignedResourceId>();\n\n /** Mark a resource for fetching. Deduplicates and respects final-resource set. */\n const requestState = (rid: OptionalSignedResourceId) => {\n if (isNullSignedResourceId(rid) || requested.has(rid)) return;\n\n if (finalResources.has(rid)) {\n if (stats) stats.finalResourcesSkipped++;\n return;\n }\n\n requested.add(rid);\n\n pending.push(\n limiter.run(async () => {\n const resourceData = tx.getResourceDataIfExists(rid, true);\n const kvData = tx.listKeyValuesIfResourceExists(rid);\n\n // counting round-trip (begin)\n const addRT = roundTripToggle;\n if (roundTripToggle) roundTripToggle = false;\n\n const [resource, kv] = await Promise.all([resourceData, kvData]);\n\n // counting round-trip, actually incrementing counter and returning toggle back,\n // so the next request can acquire it\n if (addRT) {\n numberOfRoundTrips++;\n roundTripToggle = true;\n }\n\n if (resource === undefined) return undefined;\n if (kv === undefined) throw new Error(\"Inconsistent replies\");\n\n return { ...resource, kv };\n }),\n );\n };\n\n // sending seed requests\n seedResources.forEach((rid) => requestState(rid));\n\n const result: ExtendedResourceData[] = [];\n let nextPromise: Promise<ExtendedResourceData | undefined> | undefined;\n while ((nextPromise = pending.shift()) !== undefined) {\n // at this point we pause and wait for the next requested resource state to arrive\n let nextResource = await nextPromise;\n if (nextResource === undefined)\n // ignoring resources that were not found (this may happen for seed resource ids)\n continue;\n\n if (pruningFunction !== undefined) {\n // apply field pruning, if requested\n const fieldsAfterPruning = pruningFunction(nextResource);\n // collecting stats\n if (stats) stats.prunedFields += nextResource.fields.length - fieldsAfterPruning.length;\n nextResource = { ...nextResource, fields: fieldsAfterPruning };\n }\n\n // continue traversal over the referenced resources\n requestState(nextResource.error);\n for (const field of nextResource.fields) {\n requestState(field.value);\n requestState(field.error);\n }\n\n // collecting stats\n collectStatsForResource(nextResource, stats);\n\n // aggregating the state\n result.push(nextResource);\n }\n\n if (stats) stats.roundTrips += numberOfRoundTrips;\n\n return result;\n}\n\nasync function processResourceTreeStream(\n treeItems: AsyncIterable<ResourceTreeFrame>,\n finalResources: Set<SignedResourceId>,\n pruningFunction: PruningFunction | undefined,\n stats: TreeLoadingStat | undefined,\n): Promise<{ result: ExtendedResourceData[]; followUpSeeds: SignedResourceId[] }> {\n const result: ExtendedResourceData[] = [];\n const followUpSeeds: SignedResourceId[] = [];\n\n // backend returns two types of frames:\n // - 'resource' frames contain the resource state and are processed normally\n // - 'stopMarker' frames indicate resources that are ignored due stop rules fired\n //\n // Usually stop rules indicates the resources with final state. In that case middle layer\n // should make a decision: has it already loaded the resource or should it be requested for get the latest state?\n for await (const frame of treeItems) {\n if (frame.frameKind === \"stopMarker\") {\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (finalResources.has(frame.id)) {\n if (stats) stats.finalResourcesSkipped++;\n continue;\n }\n\n let nextResource: ExtendedResourceData = {\n id: frame.id,\n type: frame.type,\n kind: frame.kind,\n data: frame.data,\n resourceReady: frame.resourceReady,\n error: frame.error,\n originalResourceId: frame.originalResourceId,\n // traverseWasStopped: backend matched traverse stop rules — children were not streamed.\n // Mark as terminal; fields are resolved below.\n final: frame.final || frame.traverseWasStopped,\n inputsLocked: frame.inputsLocked,\n outputsLocked: frame.outputsLocked,\n fields: frame.fields,\n kv: frame.kv,\n };\n\n // Apply field rules: traverseWasStopped drops all fields to keep the refCount\n // invariant; pruning function further filters the remaining fields.\n const rawFields = frame.traverseWasStopped ? [] : nextResource.fields;\n const resolvedFields =\n pruningFunction !== undefined\n ? pruningFunction({ ...nextResource, fields: rawFields })\n : rawFields;\n if (stats) stats.prunedFields += nextResource.fields.length - resolvedFields.length;\n nextResource = { ...nextResource, fields: resolvedFields };\n\n collectStatsForResource(nextResource, stats);\n result.push(nextResource);\n }\n\n return { result, followUpSeeds };\n}\n\nasync function loadTreeStateViaResourceTree(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } =\n loadingRequest;\n\n // Round 0: initial tree traversal.\n const treeItems = tx.resourceTree(seedResources, {\n includeKv: true,\n fieldFilter,\n traverseStopRules,\n });\n\n const { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) stats.roundTrips++;\n\n // Client must request full resource tree in case when stop-marker seeds are returned,\n // to ensure all resources are loaded and stop-marker frames are processed.\n if (followUpSeeds.length > 0) {\n const followUpItems = tx.resourceTree(followUpSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n result.push(...followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${followUpSeeds.length} stop-marker seeds: ${JSON.stringify(followUpSeeds)}`,\n );\n stats.roundTrips++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n }\n\n return result;\n}\n\n/** Given the transaction (preferably read-only) and loading request, executes\n * the tree traversal algorithm, and collects fresh states of resources\n * to update the tree state. */\nexport async function loadTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n capabilities: readonly string[] = [],\n mode: TraversalMode = \"auto\",\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const startTimestamp = Date.now();\n if (stats) stats.requests++;\n\n try {\n const wantsStreaming =\n mode === \"backend-streaming\" ||\n (mode === \"auto\" && supportsResourceTreeTraversal(capabilities));\n\n if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {\n const msg =\n \"traversalMode=backend-streaming but backend lacks treeFilter:v1 capability; falling back to BFS\";\n if (logger) logger.warn(msg);\n else console.warn(msg);\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n }\n\n return wantsStreaming\n ? await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger)\n : await loadTreeStateViaBfs(tx, loadingRequest, stats);\n } finally {\n if (stats) stats.millisSpent += Date.now() - startTimestamp;\n }\n}\n"],"mappings":"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"}
|
package/dist/sync.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ExtendedResourceData, PlTreeState } from "./state.js";
|
|
2
|
-
import { FieldData, PlTransaction, SignedResourceId } from "@milaboratories/pl-client";
|
|
2
|
+
import { FieldData, Filter, PlTransaction, SignedResourceId } from "@milaboratories/pl-client";
|
|
3
3
|
|
|
4
4
|
//#region src/sync.d.ts
|
|
5
5
|
/** Applied to list of fields in resource data. */
|
|
@@ -13,16 +13,24 @@ interface TreeLoadingRequest {
|
|
|
13
13
|
* Algorithm will not continue traversal over those ids, and states will not
|
|
14
14
|
* be retrieved for them. */
|
|
15
15
|
readonly finalResources: Set<SignedResourceId>;
|
|
16
|
-
/**
|
|
17
|
-
* using it continue traversal. This modification also is applied to
|
|
18
|
-
* output data to make result self-consistent in terms that it will contain
|
|
19
|
-
* all referenced resources, this is required to be able to pass it to tree
|
|
20
|
-
* to update the state. */
|
|
16
|
+
/** Applied to each resource field list in fallback BFS mode and to streamed results. */
|
|
21
17
|
readonly pruningFunction?: PruningFunction;
|
|
18
|
+
/** ResourceTree field filter passed to the backend when supported. */
|
|
19
|
+
readonly fieldFilter?: Filter;
|
|
20
|
+
/** ResourceTree traversal stop rules passed to the backend when supported. */
|
|
21
|
+
readonly traverseStopRules?: Filter;
|
|
22
22
|
}
|
|
23
|
+
/** Controls which tree-loading path is used.
|
|
24
|
+
* - `"auto"` (default): use backend streaming when the backend advertises `treeFilter:v1`,
|
|
25
|
+
* fall back to client-side BFS otherwise.
|
|
26
|
+
* - `"client-bfs"`: always use client-side BFS, even on capable backends.
|
|
27
|
+
* - `"backend-streaming"`: always prefer backend streaming; if the capability is absent,
|
|
28
|
+
* logs a warning and falls back to BFS (never throws).
|
|
29
|
+
*/
|
|
30
|
+
type TraversalMode = "auto" | "client-bfs" | "backend-streaming";
|
|
23
31
|
/** Given the current tree state, build the request object to pass to
|
|
24
32
|
* {@link loadTreeState} to load updated state. */
|
|
25
|
-
declare function constructTreeLoadingRequest(tree: PlTreeState,
|
|
33
|
+
declare function constructTreeLoadingRequest(tree: PlTreeState, options?: Pick<TreeLoadingRequest, "pruningFunction" | "fieldFilter" | "traverseStopRules">): TreeLoadingRequest;
|
|
26
34
|
type TreeLoadingStat = {
|
|
27
35
|
requests: number;
|
|
28
36
|
roundTrips: number;
|
|
@@ -33,14 +41,19 @@ type TreeLoadingStat = {
|
|
|
33
41
|
retrievedKeyValueBytes: number;
|
|
34
42
|
prunedFields: number;
|
|
35
43
|
finalResourcesSkipped: number;
|
|
36
|
-
millisSpent: number;
|
|
44
|
+
millisSpent: number; /** Stop-marker frames whose id was already final locally and were skipped. */
|
|
45
|
+
stopMarkersSkipped: number; /** Number of follow-up resourceTree() calls issued to resolve unknown stop markers. */
|
|
46
|
+
stopMarkerFollowUpRoundTrips: number;
|
|
37
47
|
};
|
|
38
48
|
declare function initialTreeLoadingStat(): TreeLoadingStat;
|
|
39
49
|
declare function formatTreeLoadingStat(stat: TreeLoadingStat): string;
|
|
40
50
|
/** Given the transaction (preferably read-only) and loading request, executes
|
|
41
51
|
* the tree traversal algorithm, and collects fresh states of resources
|
|
42
52
|
* to update the tree state. */
|
|
43
|
-
declare function loadTreeState(tx: PlTransaction, loadingRequest: TreeLoadingRequest, stats?: TreeLoadingStat
|
|
53
|
+
declare function loadTreeState(tx: PlTransaction, loadingRequest: TreeLoadingRequest, stats?: TreeLoadingStat, capabilities?: readonly string[], mode?: TraversalMode, logger?: {
|
|
54
|
+
warn: (msg: string) => void;
|
|
55
|
+
info?: (msg: unknown) => void;
|
|
56
|
+
}): Promise<ExtendedResourceData[]>;
|
|
44
57
|
//#endregion
|
|
45
|
-
export { PruningFunction, TreeLoadingRequest, TreeLoadingStat, constructTreeLoadingRequest, formatTreeLoadingStat, initialTreeLoadingStat, loadTreeState };
|
|
58
|
+
export { PruningFunction, TraversalMode, TreeLoadingRequest, TreeLoadingStat, constructTreeLoadingRequest, formatTreeLoadingStat, initialTreeLoadingStat, loadTreeState };
|
|
46
59
|
//# sourceMappingURL=sync.d.ts.map
|
package/dist/sync.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","names":[],"sources":["../src/sync.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","names":[],"sources":["../src/sync.ts"],"mappings":";;;;;KAcY,eAAA,IAAmB,QAAA,EAAU,oBAAA,KAAyB,SAAA;AAAA,UAEjD,kBAAA;EAFU;;;EAAA,SAMhB,aAAA,EAAe,gBAAA;EANK;;;EAAA,SAWpB,cAAA,EAAgB,GAAA,CAAI,gBAAA;EATd;EAAA,SAYN,eAAA,GAAkB,eAAA;;WAGlB,WAAA,GAAc,MAAA;EANM;EAAA,SASpB,iBAAA,GAAoB,MAAA;AAAA;;;;;;;;KAYnB,aAAA;;;iBAII,2BAAA,CACd,IAAA,EAAM,WAAA,EACN,OAAA,GAAS,IAAA,CAAK,kBAAA,6DACb,kBAAA;AAAA,KAoBS,eAAA;EACV,QAAA;EACA,UAAA;EACA,kBAAA;EACA,eAAA;EACA,kBAAA;EACA,0BAAA;EACA,sBAAA;EACA,YAAA;EACA,qBAAA;EACA,WAAA,UAjCc;EAmCd,kBAAA;EAEA,4BAAA;AAAA;AAAA,iBAGc,sBAAA,CAAA,GAA0B,eAAA;AAAA,iBAiB1B,qBAAA,CAAsB,IAAA,EAAM,eAAA;;;;iBAoPtB,aAAA,CACpB,EAAA,EAAI,aAAA,EACJ,cAAA,EAAgB,kBAAA,EAChB,KAAA,GAAQ,eAAA,EACR,YAAA,sBACA,IAAA,GAAM,aAAA,EACN,MAAA;EAAW,IAAA,GAAO,GAAA;EAAsB,IAAA,IAAQ,GAAA;AAAA,IAC/C,OAAA,CAAQ,oBAAA"}
|
package/dist/sync.js
CHANGED
|
@@ -2,9 +2,10 @@ import { isNullSignedResourceId } from "@milaboratories/pl-client";
|
|
|
2
2
|
import { ConcurrencyLimitingExecutor, msToHumanReadable } from "@milaboratories/ts-helpers";
|
|
3
3
|
import Denque from "denque";
|
|
4
4
|
//#region src/sync.ts
|
|
5
|
+
const CapabilityTreeFilter = "treeFilter:v1";
|
|
5
6
|
/** Given the current tree state, build the request object to pass to
|
|
6
7
|
* {@link loadTreeState} to load updated state. */
|
|
7
|
-
function constructTreeLoadingRequest(tree,
|
|
8
|
+
function constructTreeLoadingRequest(tree, options = {}) {
|
|
8
9
|
const seedResources = [];
|
|
9
10
|
const finalResources = /* @__PURE__ */ new Set();
|
|
10
11
|
tree.forEachResource((res) => {
|
|
@@ -15,7 +16,9 @@ function constructTreeLoadingRequest(tree, pruningFunction) {
|
|
|
15
16
|
return {
|
|
16
17
|
seedResources,
|
|
17
18
|
finalResources,
|
|
18
|
-
pruningFunction
|
|
19
|
+
pruningFunction: options.pruningFunction,
|
|
20
|
+
fieldFilter: options.fieldFilter,
|
|
21
|
+
traverseStopRules: options.traverseStopRules
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
24
|
function initialTreeLoadingStat() {
|
|
@@ -29,7 +32,9 @@ function initialTreeLoadingStat() {
|
|
|
29
32
|
retrievedKeyValueBytes: 0,
|
|
30
33
|
prunedFields: 0,
|
|
31
34
|
finalResourcesSkipped: 0,
|
|
32
|
-
millisSpent: 0
|
|
35
|
+
millisSpent: 0,
|
|
36
|
+
stopMarkersSkipped: 0,
|
|
37
|
+
stopMarkerFollowUpRoundTrips: 0
|
|
33
38
|
};
|
|
34
39
|
}
|
|
35
40
|
function formatTreeLoadingStat(stat) {
|
|
@@ -42,15 +47,23 @@ function formatTreeLoadingStat(stat) {
|
|
|
42
47
|
result += `Data Bytes: ${stat.retrievedResourceDataBytes}\n`;
|
|
43
48
|
result += `KV Bytes: ${stat.retrievedKeyValueBytes}\n`;
|
|
44
49
|
result += `Pruned fields: ${stat.prunedFields}\n`;
|
|
45
|
-
result += `Final resources skipped: ${stat.finalResourcesSkipped}`;
|
|
50
|
+
result += `Final resources skipped: ${stat.finalResourcesSkipped}\n`;
|
|
51
|
+
result += `Stop markers skipped: ${stat.stopMarkersSkipped}\n`;
|
|
52
|
+
result += `Stop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}`;
|
|
46
53
|
return result;
|
|
47
54
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
function supportsResourceTreeTraversal(capabilities = []) {
|
|
56
|
+
return capabilities.includes(CapabilityTreeFilter);
|
|
57
|
+
}
|
|
58
|
+
function collectStatsForResource(resource, stats) {
|
|
59
|
+
if (!stats) return;
|
|
60
|
+
stats.retrievedResources++;
|
|
61
|
+
stats.retrievedFields += resource.fields.length;
|
|
62
|
+
stats.retrievedKeyValues += resource.kv.length;
|
|
63
|
+
stats.retrievedResourceDataBytes += resource.data?.length ?? 0;
|
|
64
|
+
for (const kv of resource.kv) stats.retrievedKeyValueBytes += kv.value.length;
|
|
65
|
+
}
|
|
66
|
+
async function loadTreeStateViaBfs(tx, loadingRequest, stats) {
|
|
54
67
|
const { seedResources, finalResources, pruningFunction } = loadingRequest;
|
|
55
68
|
const limiter = new ConcurrencyLimitingExecutor(100);
|
|
56
69
|
const pending = new Denque();
|
|
@@ -102,21 +115,101 @@ async function loadTreeState(tx, loadingRequest, stats) {
|
|
|
102
115
|
requestState(field.value);
|
|
103
116
|
requestState(field.error);
|
|
104
117
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
collectStatsForResource(nextResource, stats);
|
|
119
|
+
result.push(nextResource);
|
|
120
|
+
}
|
|
121
|
+
if (stats) stats.roundTrips += numberOfRoundTrips;
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
async function processResourceTreeStream(treeItems, finalResources, pruningFunction, stats) {
|
|
125
|
+
const result = [];
|
|
126
|
+
const followUpSeeds = [];
|
|
127
|
+
for await (const frame of treeItems) {
|
|
128
|
+
if (frame.frameKind === "stopMarker") {
|
|
129
|
+
if (finalResources.has(frame.id)) {
|
|
130
|
+
if (stats) stats.stopMarkersSkipped++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
followUpSeeds.push(frame.id);
|
|
134
|
+
continue;
|
|
111
135
|
}
|
|
136
|
+
if (finalResources.has(frame.id)) {
|
|
137
|
+
if (stats) stats.finalResourcesSkipped++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
let nextResource = {
|
|
141
|
+
id: frame.id,
|
|
142
|
+
type: frame.type,
|
|
143
|
+
kind: frame.kind,
|
|
144
|
+
data: frame.data,
|
|
145
|
+
resourceReady: frame.resourceReady,
|
|
146
|
+
error: frame.error,
|
|
147
|
+
originalResourceId: frame.originalResourceId,
|
|
148
|
+
final: frame.final || frame.traverseWasStopped,
|
|
149
|
+
inputsLocked: frame.inputsLocked,
|
|
150
|
+
outputsLocked: frame.outputsLocked,
|
|
151
|
+
fields: frame.fields,
|
|
152
|
+
kv: frame.kv
|
|
153
|
+
};
|
|
154
|
+
const rawFields = frame.traverseWasStopped ? [] : nextResource.fields;
|
|
155
|
+
const resolvedFields = pruningFunction !== void 0 ? pruningFunction({
|
|
156
|
+
...nextResource,
|
|
157
|
+
fields: rawFields
|
|
158
|
+
}) : rawFields;
|
|
159
|
+
if (stats) stats.prunedFields += nextResource.fields.length - resolvedFields.length;
|
|
160
|
+
nextResource = {
|
|
161
|
+
...nextResource,
|
|
162
|
+
fields: resolvedFields
|
|
163
|
+
};
|
|
164
|
+
collectStatsForResource(nextResource, stats);
|
|
112
165
|
result.push(nextResource);
|
|
113
166
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
167
|
+
return {
|
|
168
|
+
result,
|
|
169
|
+
followUpSeeds
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async function loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger) {
|
|
173
|
+
const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } = loadingRequest;
|
|
174
|
+
const { result, followUpSeeds } = await processResourceTreeStream(tx.resourceTree(seedResources, {
|
|
175
|
+
includeKv: true,
|
|
176
|
+
fieldFilter,
|
|
177
|
+
traverseStopRules
|
|
178
|
+
}), finalResources, pruningFunction, stats);
|
|
179
|
+
if (stats) stats.roundTrips++;
|
|
180
|
+
if (followUpSeeds.length > 0) {
|
|
181
|
+
const { result: followUpResult } = await processResourceTreeStream(tx.resourceTree(followUpSeeds, {
|
|
182
|
+
includeKv: true,
|
|
183
|
+
fieldFilter
|
|
184
|
+
}), finalResources, pruningFunction, stats);
|
|
185
|
+
result.push(...followUpResult);
|
|
186
|
+
if (stats) {
|
|
187
|
+
logger?.info?.(`loadTreeStateViaResourceTree: follow-up request for ${followUpSeeds.length} stop-marker seeds: ${JSON.stringify(followUpSeeds)}`);
|
|
188
|
+
stats.roundTrips++;
|
|
189
|
+
stats.stopMarkerFollowUpRoundTrips++;
|
|
190
|
+
}
|
|
117
191
|
}
|
|
118
192
|
return result;
|
|
119
193
|
}
|
|
194
|
+
/** Given the transaction (preferably read-only) and loading request, executes
|
|
195
|
+
* the tree traversal algorithm, and collects fresh states of resources
|
|
196
|
+
* to update the tree state. */
|
|
197
|
+
async function loadTreeState(tx, loadingRequest, stats, capabilities = [], mode = "auto", logger) {
|
|
198
|
+
const startTimestamp = Date.now();
|
|
199
|
+
if (stats) stats.requests++;
|
|
200
|
+
try {
|
|
201
|
+
const wantsStreaming = mode === "backend-streaming" || mode === "auto" && supportsResourceTreeTraversal(capabilities);
|
|
202
|
+
if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {
|
|
203
|
+
const msg = "traversalMode=backend-streaming but backend lacks treeFilter:v1 capability; falling back to BFS";
|
|
204
|
+
if (logger) logger.warn(msg);
|
|
205
|
+
else console.warn(msg);
|
|
206
|
+
return await loadTreeStateViaBfs(tx, loadingRequest, stats);
|
|
207
|
+
}
|
|
208
|
+
return wantsStreaming ? await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger) : await loadTreeStateViaBfs(tx, loadingRequest, stats);
|
|
209
|
+
} finally {
|
|
210
|
+
if (stats) stats.millisSpent += Date.now() - startTimestamp;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
120
213
|
//#endregion
|
|
121
214
|
export { constructTreeLoadingRequest, formatTreeLoadingStat, initialTreeLoadingStat, loadTreeState };
|
|
122
215
|
|
package/dist/sync.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.js","names":[],"sources":["../src/sync.ts"],"sourcesContent":["import type {\n FieldData,\n OptionalSignedResourceId,\n PlTransaction,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport Denque from \"denque\";\nimport { isNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData, PlTreeState } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\n\n/** Applied to list of fields in resource data. */\nexport type PruningFunction = (resource: ExtendedResourceData) => FieldData[];\n\nexport interface TreeLoadingRequest {\n /** Resource to prime the traversal algorithm. It is ok, if some of them\n * doesn't exist anymore. Should not contain elements from final resource\n * set. */\n readonly seedResources: SignedResourceId[];\n\n /** Resource ids for which state is already known and not expected to change.\n * Algorithm will not continue traversal over those ids, and states will not\n * be retrieved for them. */\n readonly finalResources: Set<SignedResourceId>;\n\n /** This function is applied to each resource data field list, before\n * using it continue traversal. This modification also is applied to\n * output data to make result self-consistent in terms that it will contain\n * all referenced resources, this is required to be able to pass it to tree\n * to update the state. */\n readonly pruningFunction?: PruningFunction;\n}\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n pruningFunction?: PruningFunction,\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n // if tree is empty, seeding tree reconstruction from the specified root\n if (seedResources.length === 0 && finalResources.size === 0) seedResources.push(tree.root);\n\n return { seedResources, finalResources, pruningFunction };\n}\n\nexport type TreeLoadingStat = {\n requests: number;\n roundTrips: number;\n retrievedResources: number;\n retrievedFields: number;\n retrievedKeyValues: number;\n retrievedResourceDataBytes: number;\n retrievedKeyValueBytes: number;\n prunedFields: number;\n finalResourcesSkipped: number;\n millisSpent: number;\n};\n\nexport function initialTreeLoadingStat(): TreeLoadingStat {\n return {\n requests: 0,\n roundTrips: 0,\n retrievedResources: 0,\n retrievedFields: 0,\n retrievedKeyValues: 0,\n retrievedResourceDataBytes: 0,\n retrievedKeyValueBytes: 0,\n prunedFields: 0,\n finalResourcesSkipped: 0,\n millisSpent: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n let result = `Requests: ${stat.requests}\\n`;\n result += `Total time: ${msToHumanReadable(stat.millisSpent)}\\n`;\n result += `Round-trips: ${stat.roundTrips}\\n`;\n result += `Resources: ${stat.retrievedResources}\\n`;\n result += `Fields: ${stat.retrievedFields}\\n`;\n result += `KV: ${stat.retrievedKeyValues}\\n`;\n result += `Data Bytes: ${stat.retrievedResourceDataBytes}\\n`;\n result += `KV Bytes: ${stat.retrievedKeyValueBytes}\\n`;\n result += `Pruned fields: ${stat.prunedFields}\\n`;\n result += `Final resources skipped: ${stat.finalResourcesSkipped}`;\n return result;\n}\n\n/** Given the transaction (preferably read-only) and loading request, executes\n * the tree traversal algorithm, and collects fresh states of resources\n * to update the tree state. */\nexport async function loadTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n): Promise<ExtendedResourceData[]> {\n // saving start timestamp to add time spent in this function to the stats at the end of the method\n const startTimestamp = Date.now();\n\n // counting the request\n if (stats) stats.requests++;\n\n const { seedResources, finalResources, pruningFunction } = loadingRequest;\n\n // Limits the number of concurrent gRPC fetches to bound peak memory\n // from in-flight request/response buffers.\n const limiter = new ConcurrencyLimitingExecutor(100);\n\n // Promises of resource states, in the order they were requested.\n const pending = new Denque<Promise<ExtendedResourceData | undefined>>();\n\n // vars to calculate number of roundtrips for stats\n let roundTripToggle: boolean = true;\n let numberOfRoundTrips = 0;\n\n // tracking resources we already requested or queued\n const requested = new Set<SignedResourceId>();\n\n /** Mark a resource for fetching. Deduplicates and respects final-resource set. */\n const requestState = (rid: OptionalSignedResourceId) => {\n if (isNullSignedResourceId(rid) || requested.has(rid)) return;\n\n if (finalResources.has(rid)) {\n if (stats) stats.finalResourcesSkipped++;\n return;\n }\n\n requested.add(rid);\n\n pending.push(\n limiter.run(async () => {\n const resourceData = tx.getResourceDataIfExists(rid, true);\n const kvData = tx.listKeyValuesIfResourceExists(rid);\n\n // counting round-trip (begin)\n const addRT = roundTripToggle;\n if (roundTripToggle) roundTripToggle = false;\n\n const [resource, kv] = await Promise.all([resourceData, kvData]);\n\n // counting round-trip, actually incrementing counter and returning toggle back,\n // so the next request can acquire it\n if (addRT) {\n numberOfRoundTrips++;\n roundTripToggle = true;\n }\n\n if (resource === undefined) return undefined;\n if (kv === undefined) throw new Error(\"Inconsistent replies\");\n\n return { ...resource, kv };\n }),\n );\n };\n\n // sending seed requests\n seedResources.forEach((rid) => requestState(rid));\n\n const result: ExtendedResourceData[] = [];\n let nextPromise: Promise<ExtendedResourceData | undefined> | undefined;\n while ((nextPromise = pending.shift()) !== undefined) {\n // at this point we pause and wait for the next requested resource state to arrive\n let nextResource = await nextPromise;\n if (nextResource === undefined)\n // ignoring resources that were not found (this may happen for seed resource ids)\n continue;\n\n if (pruningFunction !== undefined) {\n // apply field pruning, if requested\n const fieldsAfterPruning = pruningFunction(nextResource);\n // collecting stats\n if (stats) stats.prunedFields += nextResource.fields.length - fieldsAfterPruning.length;\n nextResource = { ...nextResource, fields: fieldsAfterPruning };\n }\n\n // continue traversal over the referenced resources\n requestState(nextResource.error);\n for (const field of nextResource.fields) {\n requestState(field.value);\n requestState(field.error);\n }\n\n // collecting stats\n if (stats) {\n stats.retrievedResources++;\n stats.retrievedFields += nextResource.fields.length;\n stats.retrievedKeyValues += nextResource.kv.length;\n stats.retrievedResourceDataBytes += nextResource.data?.length ?? 0;\n for (const kv of nextResource.kv) stats.retrievedKeyValueBytes += kv.value.length;\n }\n\n // aggregating the state\n result.push(nextResource);\n }\n\n // adding the time we spent in this method to stats\n if (stats) {\n stats.millisSpent += Date.now() - startTimestamp;\n stats.roundTrips += numberOfRoundTrips;\n }\n\n return result;\n}\n"],"mappings":";;;;;;AAmCA,SAAgB,4BACd,MACA,iBACoB;CACpB,MAAM,gBAAoC,EAAE;CAC5C,MAAM,iCAAiB,IAAI,KAAuB;AAClD,MAAK,iBAAiB,QAAQ;AAC5B,MAAI,IAAI,WAAY,gBAAe,IAAI,IAAI,GAAG;MACzC,eAAc,KAAK,IAAI,GAAG;GAC/B;AAGF,KAAI,cAAc,WAAW,KAAK,eAAe,SAAS,EAAG,eAAc,KAAK,KAAK,KAAK;AAE1F,QAAO;EAAE;EAAe;EAAgB;EAAiB;;AAgB3D,SAAgB,yBAA0C;AACxD,QAAO;EACL,UAAU;EACV,YAAY;EACZ,oBAAoB;EACpB,iBAAiB;EACjB,oBAAoB;EACpB,4BAA4B;EAC5B,wBAAwB;EACxB,cAAc;EACd,uBAAuB;EACvB,aAAa;EACd;;AAGH,SAAgB,sBAAsB,MAA+B;CACnE,IAAI,SAAS,aAAa,KAAK,SAAS;AACxC,WAAU,eAAe,kBAAkB,KAAK,YAAY,CAAC;AAC7D,WAAU,gBAAgB,KAAK,WAAW;AAC1C,WAAU,cAAc,KAAK,mBAAmB;AAChD,WAAU,WAAW,KAAK,gBAAgB;AAC1C,WAAU,OAAO,KAAK,mBAAmB;AACzC,WAAU,eAAe,KAAK,2BAA2B;AACzD,WAAU,aAAa,KAAK,uBAAuB;AACnD,WAAU,kBAAkB,KAAK,aAAa;AAC9C,WAAU,4BAA4B,KAAK;AAC3C,QAAO;;;;;AAMT,eAAsB,cACpB,IACA,gBACA,OACiC;CAEjC,MAAM,iBAAiB,KAAK,KAAK;AAGjC,KAAI,MAAO,OAAM;CAEjB,MAAM,EAAE,eAAe,gBAAgB,oBAAoB;CAI3D,MAAM,UAAU,IAAI,4BAA4B,IAAI;CAGpD,MAAM,UAAU,IAAI,QAAmD;CAGvE,IAAI,kBAA2B;CAC/B,IAAI,qBAAqB;CAGzB,MAAM,4BAAY,IAAI,KAAuB;;CAG7C,MAAM,gBAAgB,QAAkC;AACtD,MAAI,uBAAuB,IAAI,IAAI,UAAU,IAAI,IAAI,CAAE;AAEvD,MAAI,eAAe,IAAI,IAAI,EAAE;AAC3B,OAAI,MAAO,OAAM;AACjB;;AAGF,YAAU,IAAI,IAAI;AAElB,UAAQ,KACN,QAAQ,IAAI,YAAY;GACtB,MAAM,eAAe,GAAG,wBAAwB,KAAK,KAAK;GAC1D,MAAM,SAAS,GAAG,8BAA8B,IAAI;GAGpD,MAAM,QAAQ;AACd,OAAI,gBAAiB,mBAAkB;GAEvC,MAAM,CAAC,UAAU,MAAM,MAAM,QAAQ,IAAI,CAAC,cAAc,OAAO,CAAC;AAIhE,OAAI,OAAO;AACT;AACA,sBAAkB;;AAGpB,OAAI,aAAa,KAAA,EAAW,QAAO,KAAA;AACnC,OAAI,OAAO,KAAA,EAAW,OAAM,IAAI,MAAM,uBAAuB;AAE7D,UAAO;IAAE,GAAG;IAAU;IAAI;IAC1B,CACH;;AAIH,eAAc,SAAS,QAAQ,aAAa,IAAI,CAAC;CAEjD,MAAM,SAAiC,EAAE;CACzC,IAAI;AACJ,SAAQ,cAAc,QAAQ,OAAO,MAAM,KAAA,GAAW;EAEpD,IAAI,eAAe,MAAM;AACzB,MAAI,iBAAiB,KAAA,EAEnB;AAEF,MAAI,oBAAoB,KAAA,GAAW;GAEjC,MAAM,qBAAqB,gBAAgB,aAAa;AAExD,OAAI,MAAO,OAAM,gBAAgB,aAAa,OAAO,SAAS,mBAAmB;AACjF,kBAAe;IAAE,GAAG;IAAc,QAAQ;IAAoB;;AAIhE,eAAa,aAAa,MAAM;AAChC,OAAK,MAAM,SAAS,aAAa,QAAQ;AACvC,gBAAa,MAAM,MAAM;AACzB,gBAAa,MAAM,MAAM;;AAI3B,MAAI,OAAO;AACT,SAAM;AACN,SAAM,mBAAmB,aAAa,OAAO;AAC7C,SAAM,sBAAsB,aAAa,GAAG;AAC5C,SAAM,8BAA8B,aAAa,MAAM,UAAU;AACjE,QAAK,MAAM,MAAM,aAAa,GAAI,OAAM,0BAA0B,GAAG,MAAM;;AAI7E,SAAO,KAAK,aAAa;;AAI3B,KAAI,OAAO;AACT,QAAM,eAAe,KAAK,KAAK,GAAG;AAClC,QAAM,cAAc;;AAGtB,QAAO"}
|
|
1
|
+
{"version":3,"file":"sync.js","names":[],"sources":["../src/sync.ts"],"sourcesContent":["import type {\n FieldData,\n Filter,\n OptionalSignedResourceId,\n PlTransaction,\n ResourceTreeFrame,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport Denque from \"denque\";\nimport { isNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData, PlTreeState } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\n\n/** Applied to list of fields in resource data. */\nexport type PruningFunction = (resource: ExtendedResourceData) => FieldData[];\n\nexport interface TreeLoadingRequest {\n /** Resource to prime the traversal algorithm. It is ok, if some of them\n * doesn't exist anymore. Should not contain elements from final resource\n * set. */\n readonly seedResources: SignedResourceId[];\n\n /** Resource ids for which state is already known and not expected to change.\n * Algorithm will not continue traversal over those ids, and states will not\n * be retrieved for them. */\n readonly finalResources: Set<SignedResourceId>;\n\n /** Applied to each resource field list in fallback BFS mode and to streamed results. */\n readonly pruningFunction?: PruningFunction;\n\n /** ResourceTree field filter passed to the backend when supported. */\n readonly fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules passed to the backend when supported. */\n readonly traverseStopRules?: Filter;\n}\n\nconst CapabilityTreeFilter = \"treeFilter:v1\";\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use backend streaming when the backend advertises `treeFilter:v1`,\n * fall back to client-side BFS otherwise.\n * - `\"client-bfs\"`: always use client-side BFS, even on capable backends.\n * - `\"backend-streaming\"`: always prefer backend streaming; if the capability is absent,\n * logs a warning and falls back to BFS (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\";\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<TreeLoadingRequest, \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\"> = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n // if tree is empty, seeding tree reconstruction from the specified root\n if (seedResources.length === 0 && finalResources.size === 0) seedResources.push(tree.root);\n\n return {\n seedResources,\n finalResources,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n };\n}\n\nexport type TreeLoadingStat = {\n requests: number;\n roundTrips: number;\n retrievedResources: number;\n retrievedFields: number;\n retrievedKeyValues: number;\n retrievedResourceDataBytes: number;\n retrievedKeyValueBytes: number;\n prunedFields: number;\n finalResourcesSkipped: number;\n millisSpent: number;\n /** Stop-marker frames whose id was already final locally and were skipped. */\n stopMarkersSkipped: number;\n /** Number of follow-up resourceTree() calls issued to resolve unknown stop markers. */\n stopMarkerFollowUpRoundTrips: number;\n};\n\nexport function initialTreeLoadingStat(): TreeLoadingStat {\n return {\n requests: 0,\n roundTrips: 0,\n retrievedResources: 0,\n retrievedFields: 0,\n retrievedKeyValues: 0,\n retrievedResourceDataBytes: 0,\n retrievedKeyValueBytes: 0,\n prunedFields: 0,\n finalResourcesSkipped: 0,\n millisSpent: 0,\n stopMarkersSkipped: 0,\n stopMarkerFollowUpRoundTrips: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n let result = `Requests: ${stat.requests}\\n`;\n result += `Total time: ${msToHumanReadable(stat.millisSpent)}\\n`;\n result += `Round-trips: ${stat.roundTrips}\\n`;\n result += `Resources: ${stat.retrievedResources}\\n`;\n result += `Fields: ${stat.retrievedFields}\\n`;\n result += `KV: ${stat.retrievedKeyValues}\\n`;\n result += `Data Bytes: ${stat.retrievedResourceDataBytes}\\n`;\n result += `KV Bytes: ${stat.retrievedKeyValueBytes}\\n`;\n result += `Pruned fields: ${stat.prunedFields}\\n`;\n result += `Final resources skipped: ${stat.finalResourcesSkipped}\\n`;\n result += `Stop markers skipped: ${stat.stopMarkersSkipped}\\n`;\n result += `Stop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}`;\n return result;\n}\n\nfunction supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return capabilities.includes(CapabilityTreeFilter);\n}\n\nfunction collectStatsForResource(resource: ExtendedResourceData, stats?: TreeLoadingStat) {\n if (!stats) return;\n stats.retrievedResources++;\n stats.retrievedFields += resource.fields.length;\n stats.retrievedKeyValues += resource.kv.length;\n stats.retrievedResourceDataBytes += resource.data?.length ?? 0;\n for (const kv of resource.kv) stats.retrievedKeyValueBytes += kv.value.length;\n}\n\nasync function loadTreeStateViaBfs(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction } = loadingRequest;\n\n // Limits the number of concurrent gRPC fetches to bound peak memory\n // from in-flight request/response buffers.\n const limiter = new ConcurrencyLimitingExecutor(100);\n\n // Promises of resource states, in the order they were requested.\n const pending = new Denque<Promise<ExtendedResourceData | undefined>>();\n\n // vars to calculate number of roundtrips for stats\n let roundTripToggle: boolean = true;\n let numberOfRoundTrips = 0;\n\n // tracking resources we already requested or queued\n const requested = new Set<SignedResourceId>();\n\n /** Mark a resource for fetching. Deduplicates and respects final-resource set. */\n const requestState = (rid: OptionalSignedResourceId) => {\n if (isNullSignedResourceId(rid) || requested.has(rid)) return;\n\n if (finalResources.has(rid)) {\n if (stats) stats.finalResourcesSkipped++;\n return;\n }\n\n requested.add(rid);\n\n pending.push(\n limiter.run(async () => {\n const resourceData = tx.getResourceDataIfExists(rid, true);\n const kvData = tx.listKeyValuesIfResourceExists(rid);\n\n // counting round-trip (begin)\n const addRT = roundTripToggle;\n if (roundTripToggle) roundTripToggle = false;\n\n const [resource, kv] = await Promise.all([resourceData, kvData]);\n\n // counting round-trip, actually incrementing counter and returning toggle back,\n // so the next request can acquire it\n if (addRT) {\n numberOfRoundTrips++;\n roundTripToggle = true;\n }\n\n if (resource === undefined) return undefined;\n if (kv === undefined) throw new Error(\"Inconsistent replies\");\n\n return { ...resource, kv };\n }),\n );\n };\n\n // sending seed requests\n seedResources.forEach((rid) => requestState(rid));\n\n const result: ExtendedResourceData[] = [];\n let nextPromise: Promise<ExtendedResourceData | undefined> | undefined;\n while ((nextPromise = pending.shift()) !== undefined) {\n // at this point we pause and wait for the next requested resource state to arrive\n let nextResource = await nextPromise;\n if (nextResource === undefined)\n // ignoring resources that were not found (this may happen for seed resource ids)\n continue;\n\n if (pruningFunction !== undefined) {\n // apply field pruning, if requested\n const fieldsAfterPruning = pruningFunction(nextResource);\n // collecting stats\n if (stats) stats.prunedFields += nextResource.fields.length - fieldsAfterPruning.length;\n nextResource = { ...nextResource, fields: fieldsAfterPruning };\n }\n\n // continue traversal over the referenced resources\n requestState(nextResource.error);\n for (const field of nextResource.fields) {\n requestState(field.value);\n requestState(field.error);\n }\n\n // collecting stats\n collectStatsForResource(nextResource, stats);\n\n // aggregating the state\n result.push(nextResource);\n }\n\n if (stats) stats.roundTrips += numberOfRoundTrips;\n\n return result;\n}\n\nasync function processResourceTreeStream(\n treeItems: AsyncIterable<ResourceTreeFrame>,\n finalResources: Set<SignedResourceId>,\n pruningFunction: PruningFunction | undefined,\n stats: TreeLoadingStat | undefined,\n): Promise<{ result: ExtendedResourceData[]; followUpSeeds: SignedResourceId[] }> {\n const result: ExtendedResourceData[] = [];\n const followUpSeeds: SignedResourceId[] = [];\n\n // backend returns two types of frames:\n // - 'resource' frames contain the resource state and are processed normally\n // - 'stopMarker' frames indicate resources that are ignored due stop rules fired\n //\n // Usually stop rules indicates the resources with final state. In that case middle layer\n // should make a decision: has it already loaded the resource or should it be requested for get the latest state?\n for await (const frame of treeItems) {\n if (frame.frameKind === \"stopMarker\") {\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (finalResources.has(frame.id)) {\n if (stats) stats.finalResourcesSkipped++;\n continue;\n }\n\n let nextResource: ExtendedResourceData = {\n id: frame.id,\n type: frame.type,\n kind: frame.kind,\n data: frame.data,\n resourceReady: frame.resourceReady,\n error: frame.error,\n originalResourceId: frame.originalResourceId,\n // traverseWasStopped: backend matched traverse stop rules — children were not streamed.\n // Mark as terminal; fields are resolved below.\n final: frame.final || frame.traverseWasStopped,\n inputsLocked: frame.inputsLocked,\n outputsLocked: frame.outputsLocked,\n fields: frame.fields,\n kv: frame.kv,\n };\n\n // Apply field rules: traverseWasStopped drops all fields to keep the refCount\n // invariant; pruning function further filters the remaining fields.\n const rawFields = frame.traverseWasStopped ? [] : nextResource.fields;\n const resolvedFields =\n pruningFunction !== undefined\n ? pruningFunction({ ...nextResource, fields: rawFields })\n : rawFields;\n if (stats) stats.prunedFields += nextResource.fields.length - resolvedFields.length;\n nextResource = { ...nextResource, fields: resolvedFields };\n\n collectStatsForResource(nextResource, stats);\n result.push(nextResource);\n }\n\n return { result, followUpSeeds };\n}\n\nasync function loadTreeStateViaResourceTree(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } =\n loadingRequest;\n\n // Round 0: initial tree traversal.\n const treeItems = tx.resourceTree(seedResources, {\n includeKv: true,\n fieldFilter,\n traverseStopRules,\n });\n\n const { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) stats.roundTrips++;\n\n // Client must request full resource tree in case when stop-marker seeds are returned,\n // to ensure all resources are loaded and stop-marker frames are processed.\n if (followUpSeeds.length > 0) {\n const followUpItems = tx.resourceTree(followUpSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n result.push(...followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${followUpSeeds.length} stop-marker seeds: ${JSON.stringify(followUpSeeds)}`,\n );\n stats.roundTrips++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n }\n\n return result;\n}\n\n/** Given the transaction (preferably read-only) and loading request, executes\n * the tree traversal algorithm, and collects fresh states of resources\n * to update the tree state. */\nexport async function loadTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n capabilities: readonly string[] = [],\n mode: TraversalMode = \"auto\",\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const startTimestamp = Date.now();\n if (stats) stats.requests++;\n\n try {\n const wantsStreaming =\n mode === \"backend-streaming\" ||\n (mode === \"auto\" && supportsResourceTreeTraversal(capabilities));\n\n if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {\n const msg =\n \"traversalMode=backend-streaming but backend lacks treeFilter:v1 capability; falling back to BFS\";\n if (logger) logger.warn(msg);\n else console.warn(msg);\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n }\n\n return wantsStreaming\n ? await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger)\n : await loadTreeStateViaBfs(tx, loadingRequest, stats);\n } finally {\n if (stats) stats.millisSpent += Date.now() - startTimestamp;\n }\n}\n"],"mappings":"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"}
|