@milaboratories/pl-tree 1.11.1 → 1.12.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/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:v1");
59
58
  }
60
59
  function collectStatsForResource(resource, stats) {
61
60
  if (!stats) return;
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: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 hasCapability(capabilities, \"treeFilter:v1\");\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 +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:v1");
57
56
  }
58
57
  function collectStatsForResource(resource, stats) {
59
58
  if (!stats) return;
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: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 hasCapability(capabilities, \"treeFilter:v1\");\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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-tree",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "Reactive pl tree state",
5
5
  "files": [
6
6
  "./dist/**/*",
@@ -22,8 +22,8 @@
22
22
  "utility-types": "^3.11.0",
23
23
  "zod": "~3.25.76",
24
24
  "@milaboratories/computable": "2.9.4",
25
- "@milaboratories/pl-errors": "1.4.8",
26
- "@milaboratories/pl-client": "3.6.0",
25
+ "@milaboratories/pl-client": "3.8.0",
26
+ "@milaboratories/pl-errors": "1.4.10",
27
27
  "@milaboratories/ts-helpers": "1.8.2"
28
28
  },
29
29
  "devDependencies": {
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,8 +35,6 @@ 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
39
  * - `"auto"` (default): use backend streaming when the backend advertises `treeFilter:v1`,
42
40
  * fall back to client-side BFS otherwise.
@@ -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:v1");
126
124
  }
127
125
 
128
126
  function collectStatsForResource(resource: ExtendedResourceData, stats?: TreeLoadingStat) {