@milaboratories/pl-tree 1.12.0 → 1.12.2

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
@@ -54,7 +54,7 @@ function formatTreeLoadingStat(stat) {
54
54
  return result;
55
55
  }
56
56
  function supportsResourceTreeTraversal(capabilities = []) {
57
- return (0, _milaboratories_pl_client.hasCapability)(capabilities, "treeFilter:v1");
57
+ return (0, _milaboratories_pl_client.hasCapability)(capabilities, "treeFilter:v2");
58
58
  }
59
59
  function collectStatsForResource(resource, stats) {
60
60
  if (!stats) return;
@@ -201,7 +201,7 @@ async function loadTreeState(tx, loadingRequest, stats, capabilities = [], mode
201
201
  try {
202
202
  const wantsStreaming = mode === "backend-streaming" || mode === "auto" && supportsResourceTreeTraversal(capabilities);
203
203
  if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {
204
- 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";
205
205
  if (logger) logger.warn(msg);
206
206
  else console.warn(msg);
207
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 { 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
+ {"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,
package/dist/sync.js CHANGED
@@ -52,7 +52,7 @@ function formatTreeLoadingStat(stat) {
52
52
  return result;
53
53
  }
54
54
  function supportsResourceTreeTraversal(capabilities = []) {
55
- return hasCapability(capabilities, "treeFilter:v1");
55
+ return hasCapability(capabilities, "treeFilter:v2");
56
56
  }
57
57
  function collectStatsForResource(resource, stats) {
58
58
  if (!stats) return;
@@ -199,7 +199,7 @@ async function loadTreeState(tx, loadingRequest, stats, capabilities = [], mode
199
199
  try {
200
200
  const wantsStreaming = mode === "backend-streaming" || mode === "auto" && supportsResourceTreeTraversal(capabilities);
201
201
  if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {
202
- 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";
203
203
  if (logger) logger.warn(msg);
204
204
  else console.warn(msg);
205
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 { 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
+ {"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":";;;;;;AAgDA,SAAgB,4BACd,MACA,UAA6F,EAAE,EAC3E;CACpB,MAAM,gBAAoC,EAAE;CAC5C,MAAM,iCAAiB,IAAI,KAAuB;AAClD,MAAK,iBAAiB,QAAQ;AAC5B,MAAI,IAAI,WAAY,gBAAe,IAAI,IAAI,GAAG;MACzC,eAAc,KAAK,IAAI,GAAG;GAC/B;AAGF,KAAI,cAAc,WAAW,KAAK,eAAe,SAAS,EAAG,eAAc,KAAK,KAAK,KAAK;AAE1F,QAAO;EACL;EACA;EACA,iBAAiB,QAAQ;EACzB,aAAa,QAAQ;EACrB,mBAAmB,QAAQ;EAC5B;;AAoBH,SAAgB,yBAA0C;AACxD,QAAO;EACL,UAAU;EACV,YAAY;EACZ,oBAAoB;EACpB,iBAAiB;EACjB,oBAAoB;EACpB,4BAA4B;EAC5B,wBAAwB;EACxB,cAAc;EACd,uBAAuB;EACvB,aAAa;EACb,oBAAoB;EACpB,8BAA8B;EAC/B;;AAGH,SAAgB,sBAAsB,MAA+B;CACnE,IAAI,SAAS,aAAa,KAAK,SAAS;AACxC,WAAU,eAAe,kBAAkB,KAAK,YAAY,CAAC;AAC7D,WAAU,gBAAgB,KAAK,WAAW;AAC1C,WAAU,cAAc,KAAK,mBAAmB;AAChD,WAAU,WAAW,KAAK,gBAAgB;AAC1C,WAAU,OAAO,KAAK,mBAAmB;AACzC,WAAU,eAAe,KAAK,2BAA2B;AACzD,WAAU,aAAa,KAAK,uBAAuB;AACnD,WAAU,kBAAkB,KAAK,aAAa;AAC9C,WAAU,4BAA4B,KAAK,sBAAsB;AACjE,WAAU,yBAAyB,KAAK,mBAAmB;AAC3D,WAAU,sCAAsC,KAAK;AACrD,QAAO;;AAGT,SAAS,8BAA8B,eAAkC,EAAE,EAAW;AACpF,QAAO,cAAc,cAAc,gBAAgB;;AAGrD,SAAS,wBAAwB,UAAgC,OAAyB;AACxF,KAAI,CAAC,MAAO;AACZ,OAAM;AACN,OAAM,mBAAmB,SAAS,OAAO;AACzC,OAAM,sBAAsB,SAAS,GAAG;AACxC,OAAM,8BAA8B,SAAS,MAAM,UAAU;AAC7D,MAAK,MAAM,MAAM,SAAS,GAAI,OAAM,0BAA0B,GAAG,MAAM;;AAGzE,eAAe,oBACb,IACA,gBACA,OACiC;CACjC,MAAM,EAAE,eAAe,gBAAgB,oBAAoB;CAI3D,MAAM,UAAU,IAAI,4BAA4B,IAAI;CAGpD,MAAM,UAAU,IAAI,QAAmD;CAGvE,IAAI,kBAA2B;CAC/B,IAAI,qBAAqB;CAGzB,MAAM,4BAAY,IAAI,KAAuB;;CAG7C,MAAM,gBAAgB,QAAkC;AACtD,MAAI,uBAAuB,IAAI,IAAI,UAAU,IAAI,IAAI,CAAE;AAEvD,MAAI,eAAe,IAAI,IAAI,EAAE;AAC3B,OAAI,MAAO,OAAM;AACjB;;AAGF,YAAU,IAAI,IAAI;AAElB,UAAQ,KACN,QAAQ,IAAI,YAAY;GACtB,MAAM,eAAe,GAAG,wBAAwB,KAAK,KAAK;GAC1D,MAAM,SAAS,GAAG,8BAA8B,IAAI;GAGpD,MAAM,QAAQ;AACd,OAAI,gBAAiB,mBAAkB;GAEvC,MAAM,CAAC,UAAU,MAAM,MAAM,QAAQ,IAAI,CAAC,cAAc,OAAO,CAAC;AAIhE,OAAI,OAAO;AACT;AACA,sBAAkB;;AAGpB,OAAI,aAAa,KAAA,EAAW,QAAO,KAAA;AACnC,OAAI,OAAO,KAAA,EAAW,OAAM,IAAI,MAAM,uBAAuB;AAE7D,UAAO;IAAE,GAAG;IAAU;IAAI;IAC1B,CACH;;AAIH,eAAc,SAAS,QAAQ,aAAa,IAAI,CAAC;CAEjD,MAAM,SAAiC,EAAE;CACzC,IAAI;AACJ,SAAQ,cAAc,QAAQ,OAAO,MAAM,KAAA,GAAW;EAEpD,IAAI,eAAe,MAAM;AACzB,MAAI,iBAAiB,KAAA,EAEnB;AAEF,MAAI,oBAAoB,KAAA,GAAW;GAEjC,MAAM,qBAAqB,gBAAgB,aAAa;AAExD,OAAI,MAAO,OAAM,gBAAgB,aAAa,OAAO,SAAS,mBAAmB;AACjF,kBAAe;IAAE,GAAG;IAAc,QAAQ;IAAoB;;AAIhE,eAAa,aAAa,MAAM;AAChC,OAAK,MAAM,SAAS,aAAa,QAAQ;AACvC,gBAAa,MAAM,MAAM;AACzB,gBAAa,MAAM,MAAM;;AAI3B,0BAAwB,cAAc,MAAM;AAG5C,SAAO,KAAK,aAAa;;AAG3B,KAAI,MAAO,OAAM,cAAc;AAE/B,QAAO;;AAGT,eAAe,0BACb,WACA,gBACA,iBACA,OACgF;CAChF,MAAM,SAAiC,EAAE;CACzC,MAAM,gBAAoC,EAAE;AAQ5C,YAAW,MAAM,SAAS,WAAW;AACnC,MAAI,MAAM,cAAc,cAAc;AACpC,OAAI,eAAe,IAAI,MAAM,GAAG,EAAE;AAChC,QAAI,MAAO,OAAM;AACjB;;AAEF,iBAAc,KAAK,MAAM,GAAG;AAC5B;;AAIF,MAAI,eAAe,IAAI,MAAM,GAAG,EAAE;AAChC,OAAI,MAAO,OAAM;AACjB;;EAGF,IAAI,eAAqC;GACvC,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,MAAM,MAAM;GACZ,MAAM,MAAM;GACZ,eAAe,MAAM;GACrB,OAAO,MAAM;GACb,oBAAoB,MAAM;GAG1B,OAAO,MAAM,SAAS,MAAM;GAC5B,cAAc,MAAM;GACpB,eAAe,MAAM;GACrB,QAAQ,MAAM;GACd,IAAI,MAAM;GACX;EAID,MAAM,YAAY,MAAM,qBAAqB,EAAE,GAAG,aAAa;EAC/D,MAAM,iBACJ,oBAAoB,KAAA,IAChB,gBAAgB;GAAE,GAAG;GAAc,QAAQ;GAAW,CAAC,GACvD;AACN,MAAI,MAAO,OAAM,gBAAgB,aAAa,OAAO,SAAS,eAAe;AAC7E,iBAAe;GAAE,GAAG;GAAc,QAAQ;GAAgB;AAE1D,0BAAwB,cAAc,MAAM;AAC5C,SAAO,KAAK,aAAa;;AAG3B,QAAO;EAAE;EAAQ;EAAe;;AAGlC,eAAe,6BACb,IACA,gBACA,OACA,QACiC;CACjC,MAAM,EAAE,eAAe,gBAAgB,iBAAiB,aAAa,sBACnE;CASF,MAAM,EAAE,QAAQ,kBAAkB,MAAM,0BANtB,GAAG,aAAa,eAAe;EAC/C,WAAW;EACX;EACA;EACD,CAAC,EAIA,gBACA,iBACA,MACD;AACD,KAAI,MAAO,OAAM;AAIjB,KAAI,cAAc,SAAS,GAAG;EAK5B,MAAM,EAAE,QAAQ,mBAAmB,MAAM,0BAJnB,GAAG,aAAa,eAAe;GACnD,WAAW;GACX;GACD,CAAC,EAGA,gBACA,iBACA,MACD;AACD,SAAO,KAAK,GAAG,eAAe;AAC9B,MAAI,OAAO;AACT,WAAQ,OACN,uDAAuD,cAAc,OAAO,sBAAsB,KAAK,UAAU,cAAc,GAChI;AACD,SAAM;AACN,SAAM;;;AAIV,QAAO;;;;;AAMT,eAAsB,cACpB,IACA,gBACA,OACA,eAAkC,EAAE,EACpC,OAAsB,QACtB,QACiC;CACjC,MAAM,iBAAiB,KAAK,KAAK;AACjC,KAAI,MAAO,OAAM;AAEjB,KAAI;EACF,MAAM,iBACJ,SAAS,uBACR,SAAS,UAAU,8BAA8B,aAAa;AAEjE,MAAI,kBAAkB,CAAC,8BAA8B,aAAa,EAAE;GAClE,MAAM,MACJ;AACF,OAAI,OAAQ,QAAO,KAAK,IAAI;OACvB,SAAQ,KAAK,IAAI;AACtB,UAAO,MAAM,oBAAoB,IAAI,gBAAgB,MAAM;;AAG7D,SAAO,iBACH,MAAM,6BAA6B,IAAI,gBAAgB,OAAO,OAAO,GACrE,MAAM,oBAAoB,IAAI,gBAAgB,MAAM;WAChD;AACR,MAAI,MAAO,OAAM,eAAe,KAAK,KAAK,GAAG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-tree",
3
- "version": "1.12.0",
3
+ "version": "1.12.2",
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-client": "3.8.0",
26
- "@milaboratories/pl-errors": "1.4.10",
25
+ "@milaboratories/pl-errors": "1.4.12",
26
+ "@milaboratories/pl-client": "3.9.0",
27
27
  "@milaboratories/ts-helpers": "1.8.2"
28
28
  },
29
29
  "devDependencies": {
@@ -31,9 +31,9 @@
31
31
  "@vitest/coverage-istanbul": "^4.1.3",
32
32
  "typescript": "~5.9.3",
33
33
  "vitest": "^4.1.3",
34
+ "@milaboratories/ts-configs": "1.2.3",
34
35
  "@milaboratories/build-configs": "2.0.0",
35
- "@milaboratories/ts-builder": "1.5.0",
36
- "@milaboratories/ts-configs": "1.2.3"
36
+ "@milaboratories/ts-builder": "1.5.0"
37
37
  },
38
38
  "engines": {
39
39
  "node": ">=22.19.0"
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
@@ -36,7 +36,7 @@ export interface TreeLoadingRequest {
36
36
  }
37
37
 
38
38
  /** Controls which tree-loading path is used.
39
- * - `"auto"` (default): use backend streaming when the backend advertises `treeFilter:v1`,
39
+ * - `"auto"` (default): use backend streaming when the backend advertises `treeFilter:v2`,
40
40
  * fall back to client-side BFS otherwise.
41
41
  * - `"client-bfs"`: always use client-side BFS, even on capable backends.
42
42
  * - `"backend-streaming"`: always prefer backend streaming; if the capability is absent,
@@ -120,7 +120,7 @@ export function formatTreeLoadingStat(stat: TreeLoadingStat): string {
120
120
  }
121
121
 
122
122
  function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {
123
- return hasCapability(capabilities, "treeFilter:v1");
123
+ return hasCapability(capabilities, "treeFilter:v2");
124
124
  }
125
125
 
126
126
  function collectStatsForResource(resource: ExtendedResourceData, stats?: TreeLoadingStat) {
@@ -365,7 +365,7 @@ export async function loadTreeState(
365
365
 
366
366
  if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {
367
367
  const msg =
368
- "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";
369
369
  if (logger) logger.warn(msg);
370
370
  else console.warn(msg);
371
371
  return await loadTreeStateViaBfs(tx, loadingRequest, stats);