@milaboratories/pl-tree 1.11.2 → 1.12.1

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/sync.cjs CHANGED
@@ -4,7 +4,6 @@ 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";
8
7
  /** Given the current tree state, build the request object to pass to
9
8
  * {@link loadTreeState} to load updated state. */
10
9
  function constructTreeLoadingRequest(tree, options = {}) {
@@ -55,7 +54,7 @@ function formatTreeLoadingStat(stat) {
55
54
  return result;
56
55
  }
57
56
  function supportsResourceTreeTraversal(capabilities = []) {
58
- return capabilities.includes(CapabilityTreeFilter);
57
+ return (0, _milaboratories_pl_client.hasCapability)(capabilities, "treeFilter:v2");
59
58
  }
60
59
  function collectStatsForResource(resource, stats) {
61
60
  if (!stats) return;
@@ -202,7 +201,7 @@ async function loadTreeState(tx, loadingRequest, stats, capabilities = [], mode
202
201
  try {
203
202
  const wantsStreaming = mode === "backend-streaming" || mode === "auto" && supportsResourceTreeTraversal(capabilities);
204
203
  if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {
205
- const msg = "traversalMode=backend-streaming but backend lacks treeFilter:v1 capability; falling back to BFS";
204
+ const msg = "traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS";
206
205
  if (logger) logger.warn(msg);
207
206
  else console.warn(msg);
208
207
  return await loadTreeStateViaBfs(tx, loadingRequest, stats);
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 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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"}
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 { hasCapability, 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\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use backend streaming when the backend advertises `treeFilter:v2`,\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 hasCapability(capabilities, \"treeFilter:v2\");\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:v2 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
@@ -21,7 +21,7 @@ interface TreeLoadingRequest {
21
21
  readonly traverseStopRules?: Filter;
22
22
  }
23
23
  /** Controls which tree-loading path is used.
24
- * - `"auto"` (default): use backend streaming when the backend advertises `treeFilter:v1`,
24
+ * - `"auto"` (default): use backend streaming when the backend advertises `treeFilter:v2`,
25
25
  * fall back to client-side BFS otherwise.
26
26
  * - `"client-bfs"`: always use client-side BFS, even on capable backends.
27
27
  * - `"backend-streaming"`: always prefer backend streaming; if the capability is absent,
@@ -1 +1 @@
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"}
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;;;;;;;;KAUnB,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
@@ -1,8 +1,7 @@
1
- import { isNullSignedResourceId } from "@milaboratories/pl-client";
1
+ import { hasCapability, 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";
6
5
  /** Given the current tree state, build the request object to pass to
7
6
  * {@link loadTreeState} to load updated state. */
8
7
  function constructTreeLoadingRequest(tree, options = {}) {
@@ -53,7 +52,7 @@ function formatTreeLoadingStat(stat) {
53
52
  return result;
54
53
  }
55
54
  function supportsResourceTreeTraversal(capabilities = []) {
56
- return capabilities.includes(CapabilityTreeFilter);
55
+ return hasCapability(capabilities, "treeFilter:v2");
57
56
  }
58
57
  function collectStatsForResource(resource, stats) {
59
58
  if (!stats) return;
@@ -200,7 +199,7 @@ async function loadTreeState(tx, loadingRequest, stats, capabilities = [], mode
200
199
  try {
201
200
  const wantsStreaming = mode === "backend-streaming" || mode === "auto" && supportsResourceTreeTraversal(capabilities);
202
201
  if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {
203
- const msg = "traversalMode=backend-streaming but backend lacks treeFilter:v1 capability; falling back to BFS";
202
+ const msg = "traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS";
204
203
  if (logger) logger.warn(msg);
205
204
  else console.warn(msg);
206
205
  return await loadTreeStateViaBfs(tx, loadingRequest, stats);
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 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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"}
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 { hasCapability, 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\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use backend streaming when the backend advertises `treeFilter:v2`,\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 hasCapability(capabilities, \"treeFilter:v2\");\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:v2 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-tree",
3
- "version": "1.11.2",
3
+ "version": "1.12.1",
4
4
  "description": "Reactive pl tree state",
5
5
  "files": [
6
6
  "./dist/**/*",
@@ -21,18 +21,18 @@
21
21
  "denque": "^2.1.0",
22
22
  "utility-types": "^3.11.0",
23
23
  "zod": "~3.25.76",
24
- "@milaboratories/pl-errors": "1.4.9",
25
- "@milaboratories/pl-client": "3.7.0",
24
+ "@milaboratories/computable": "2.9.4",
25
+ "@milaboratories/pl-client": "3.8.1",
26
26
  "@milaboratories/ts-helpers": "1.8.2",
27
- "@milaboratories/computable": "2.9.4"
27
+ "@milaboratories/pl-errors": "1.4.11"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "~24.5.2",
31
31
  "@vitest/coverage-istanbul": "^4.1.3",
32
32
  "typescript": "~5.9.3",
33
33
  "vitest": "^4.1.3",
34
- "@milaboratories/ts-builder": "1.5.0",
35
34
  "@milaboratories/build-configs": "2.0.0",
35
+ "@milaboratories/ts-builder": "1.5.0",
36
36
  "@milaboratories/ts-configs": "1.2.3"
37
37
  },
38
38
  "engines": {
package/src/sync.test.ts CHANGED
@@ -71,7 +71,7 @@ test("loadTreeState uses ResourceTree path with full options", async () => {
71
71
  traverseStopRules: {},
72
72
  } as unknown as Parameters<typeof loadTreeState>[1];
73
73
 
74
- const result = await loadTreeState(tx, request, undefined, ["treeFilter:v1"]);
74
+ const result = await loadTreeState(tx, request, undefined, ["treeFilter:v2"]);
75
75
 
76
76
  expect(received).toEqual({
77
77
  seeds: ["NG:0x1", "NG:0x2"],
@@ -96,7 +96,7 @@ test("loadTreeState propagates ResourceTree stream failure", async () => {
96
96
  finalResources: new Set<string>(),
97
97
  } as unknown as Parameters<typeof loadTreeState>[1];
98
98
 
99
- await expect(loadTreeState(tx, request, undefined, ["treeFilter:v1"])).rejects.toThrow(
99
+ await expect(loadTreeState(tx, request, undefined, ["treeFilter:v2"])).rejects.toThrow(
100
100
  "stream failed",
101
101
  );
102
102
  });
@@ -150,7 +150,7 @@ test("loadTreeState cancels ResourceTree iterator on pruning failure", async ()
150
150
  },
151
151
  } as unknown as Parameters<typeof loadTreeState>[1];
152
152
 
153
- await expect(loadTreeState(tx, request, undefined, ["treeFilter:v1"])).rejects.toThrow(
153
+ await expect(loadTreeState(tx, request, undefined, ["treeFilter:v2"])).rejects.toThrow(
154
154
  "pruning failed",
155
155
  );
156
156
  expect(returnCalled).toBe(true);
@@ -342,7 +342,7 @@ test("traversalMode=client-bfs forces BFS on capable backend", async () => {
342
342
  const { tx, calls } = buildMockTx({ capable: true });
343
343
  const mode: TraversalMode = "client-bfs";
344
344
 
345
- const result = await loadTreeState(tx, baseRequest, undefined, ["treeFilter:v1"], mode);
345
+ const result = await loadTreeState(tx, baseRequest, undefined, ["treeFilter:v2"], mode);
346
346
 
347
347
  expect(result).toHaveLength(1);
348
348
  expect(calls.bfs).toBeGreaterThan(0);
@@ -362,14 +362,14 @@ test("traversalMode=backend-streaming falls back to BFS when capability is absen
362
362
  expect(calls.bfs).toBeGreaterThan(0);
363
363
  expect(calls.streaming).toBe(0);
364
364
  expect(warnings).toHaveLength(1);
365
- expect(warnings[0]).toMatch(/treeFilter:v1/);
365
+ expect(warnings[0]).toMatch(/treeFilter:v2/);
366
366
  });
367
367
 
368
368
  test("traversalMode=backend-streaming uses streaming on capable backend", async () => {
369
369
  const { tx, calls } = buildMockTx({ capable: true });
370
370
  const mode: TraversalMode = "backend-streaming";
371
371
 
372
- const result = await loadTreeState(tx, baseRequest, undefined, ["treeFilter:v1"], mode);
372
+ const result = await loadTreeState(tx, baseRequest, undefined, ["treeFilter:v2"], mode);
373
373
 
374
374
  expect(result).toHaveLength(1);
375
375
  expect(calls.streaming).toBeGreaterThan(0);
@@ -379,7 +379,7 @@ test("traversalMode=backend-streaming uses streaming on capable backend", async
379
379
  test("traversalMode=auto default regression: streaming on capable, BFS on incapable", async () => {
380
380
  // capable backend → streaming
381
381
  const { tx: txCap, calls: callsCap } = buildMockTx({ capable: true });
382
- await loadTreeState(txCap, baseRequest, undefined, ["treeFilter:v1"]);
382
+ await loadTreeState(txCap, baseRequest, undefined, ["treeFilter:v2"]);
383
383
  expect(callsCap.streaming).toBeGreaterThan(0);
384
384
  expect(callsCap.bfs).toBe(0);
385
385
 
@@ -393,7 +393,7 @@ test("traversalMode=auto default regression: streaming on capable, BFS on incapa
393
393
  // ─── Stop-marker handling tests ───────────────────────────────────────────────
394
394
 
395
395
  /** Capabilities for backends that support streaming traversal. */
396
- const stopMarkerCaps = ["treeFilter:v1"] as const;
396
+ const stopMarkerCaps = ["treeFilter:v2"] as const;
397
397
 
398
398
  /** Minimal full-resource frame suitable for stop-marker tests. */
399
399
  function makeFullResource(
@@ -499,7 +499,7 @@ test("legacy-backend-compat: no stop markers in stream → no follow-up call", a
499
499
  } as unknown as Parameters<typeof loadTreeState>[1];
500
500
 
501
501
  const stats = initialTreeLoadingStat();
502
- const result = await loadTreeState(tx, request, stats, ["treeFilter:v1"]);
502
+ const result = await loadTreeState(tx, request, stats, ["treeFilter:v2"]);
503
503
 
504
504
  expect(result).toHaveLength(1);
505
505
  expect(callCount).toBe(1);
package/src/sync.ts CHANGED
@@ -7,7 +7,7 @@ import type {
7
7
  SignedResourceId,
8
8
  } from "@milaboratories/pl-client";
9
9
  import Denque from "denque";
10
- import { isNullSignedResourceId } from "@milaboratories/pl-client";
10
+ import { hasCapability, isNullSignedResourceId } from "@milaboratories/pl-client";
11
11
  import type { ExtendedResourceData, PlTreeState } from "./state";
12
12
  import { ConcurrencyLimitingExecutor, msToHumanReadable } from "@milaboratories/ts-helpers";
13
13
 
@@ -35,10 +35,8 @@ export interface TreeLoadingRequest {
35
35
  readonly traverseStopRules?: Filter;
36
36
  }
37
37
 
38
- const CapabilityTreeFilter = "treeFilter:v1";
39
-
40
38
  /** Controls which tree-loading path is used.
41
- * - `"auto"` (default): use backend streaming when the backend advertises `treeFilter:v1`,
39
+ * - `"auto"` (default): use backend streaming when the backend advertises `treeFilter:v2`,
42
40
  * fall back to client-side BFS otherwise.
43
41
  * - `"client-bfs"`: always use client-side BFS, even on capable backends.
44
42
  * - `"backend-streaming"`: always prefer backend streaming; if the capability is absent,
@@ -122,7 +120,7 @@ export function formatTreeLoadingStat(stat: TreeLoadingStat): string {
122
120
  }
123
121
 
124
122
  function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {
125
- return capabilities.includes(CapabilityTreeFilter);
123
+ return hasCapability(capabilities, "treeFilter:v2");
126
124
  }
127
125
 
128
126
  function collectStatsForResource(resource: ExtendedResourceData, stats?: TreeLoadingStat) {
@@ -367,7 +365,7 @@ export async function loadTreeState(
367
365
 
368
366
  if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {
369
367
  const msg =
370
- "traversalMode=backend-streaming but backend lacks treeFilter:v1 capability; falling back to BFS";
368
+ "traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS";
371
369
  if (logger) logger.warn(msg);
372
370
  else console.warn(msg);
373
371
  return await loadTreeStateViaBfs(tx, loadingRequest, stats);