@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 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, pruningFunction) {
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
- /** Given the transaction (preferably read-only) and loading request, executes
51
- * the tree traversal algorithm, and collects fresh states of resources
52
- * to update the tree state. */
53
- async function loadTreeState(tx, loadingRequest, stats) {
54
- const startTimestamp = Date.now();
55
- if (stats) stats.requests++;
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
- if (stats) {
108
- stats.retrievedResources++;
109
- stats.retrievedFields += nextResource.fields.length;
110
- stats.retrievedKeyValues += nextResource.kv.length;
111
- stats.retrievedResourceDataBytes += nextResource.data?.length ?? 0;
112
- for (const kv of nextResource.kv) stats.retrievedKeyValueBytes += kv.value.length;
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
- if (stats) {
117
- stats.millisSpent += Date.now() - startTimestamp;
118
- stats.roundTrips += numberOfRoundTrips;
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
- /** This function is applied to each resource data field list, before
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, pruningFunction?: PruningFunction): TreeLoadingRequest;
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): Promise<ExtendedResourceData[]>;
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
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","names":[],"sources":["../src/sync.ts"],"mappings":";;;;;KAYY,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,SAgBN,eAAA,GAAkB,eAAA;AAAA;;;iBAKb,2BAAA,CACd,IAAA,EAAM,WAAA,EACN,eAAA,GAAkB,eAAA,GACjB,kBAAA;AAAA,KAcS,eAAA;EACV,QAAA;EACA,UAAA;EACA,kBAAA;EACA,eAAA;EACA,kBAAA;EACA,0BAAA;EACA,sBAAA;EACA,YAAA;EACA,qBAAA;EACA,WAAA;AAAA;AAAA,iBAGc,sBAAA,CAAA,GAA0B,eAAA;AAAA,iBAe1B,qBAAA,CAAsB,IAAA,EAAM,eAAA;;;;iBAiBtB,aAAA,CACpB,EAAA,EAAI,aAAA,EACJ,cAAA,EAAgB,kBAAA,EAChB,KAAA,GAAQ,eAAA,GACP,OAAA,CAAQ,oBAAA"}
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, pruningFunction) {
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
- /** Given the transaction (preferably read-only) and loading request, executes
49
- * the tree traversal algorithm, and collects fresh states of resources
50
- * to update the tree state. */
51
- async function loadTreeState(tx, loadingRequest, stats) {
52
- const startTimestamp = Date.now();
53
- if (stats) stats.requests++;
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
- if (stats) {
106
- stats.retrievedResources++;
107
- stats.retrievedFields += nextResource.fields.length;
108
- stats.retrievedKeyValues += nextResource.kv.length;
109
- stats.retrievedResourceDataBytes += nextResource.data?.length ?? 0;
110
- for (const kv of nextResource.kv) stats.retrievedKeyValueBytes += kv.value.length;
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
- if (stats) {
115
- stats.millisSpent += Date.now() - startTimestamp;
116
- stats.roundTrips += numberOfRoundTrips;
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":"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"}
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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"}