@milaboratories/pl-tree 1.15.7 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accessors.cjs +6 -2
- package/dist/accessors.cjs.map +1 -1
- package/dist/accessors.d.ts.map +1 -1
- package/dist/accessors.js +6 -2
- package/dist/accessors.js.map +1 -1
- package/dist/delta_sync.cjs +18 -8
- package/dist/delta_sync.cjs.map +1 -1
- package/dist/delta_sync.js +18 -8
- package/dist/delta_sync.js.map +1 -1
- package/dist/state.cjs +108 -30
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.ts +27 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +108 -30
- package/dist/state.js.map +1 -1
- package/dist/sync.cjs +2 -1
- package/dist/sync.cjs.map +1 -1
- package/dist/sync.d.ts +2 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +2 -1
- package/dist/sync.js.map +1 -1
- package/dist/synchronized_tree.cjs +46 -9
- package/dist/synchronized_tree.cjs.map +1 -1
- package/dist/synchronized_tree.d.ts +6 -0
- package/dist/synchronized_tree.d.ts.map +1 -1
- package/dist/synchronized_tree.js +47 -10
- package/dist/synchronized_tree.js.map +1 -1
- package/package.json +4 -4
- package/src/accessors.test.ts +108 -0
- package/src/accessors.ts +8 -3
- package/src/delta_sync.test.ts +88 -3
- package/src/delta_sync.ts +19 -8
- package/src/state.test.ts +397 -2
- package/src/state.ts +137 -46
- package/src/state_notifications.model.test.ts +264 -0
- package/src/sync.ts +6 -3
- package/src/synchronized_tree.test.ts +550 -1
- package/src/synchronized_tree.ts +102 -20
package/dist/sync.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.cjs","names":["ConcurrencyLimitingExecutor","Denque","loadDeltaTreeState"],"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, ResourceUpdateStat } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\nimport { loadDeltaTreeState } from \"./delta_sync\";\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 * @deprecated prune with {@link changedSinceToken} instead; ignored by the backend under one. */\n readonly traverseStopRules?: Filter;\n\n /** The tree's roots. Delta seeds at these only when the non-final frontier is empty;\n * resourceTree requires at least one seed. */\n readonly roots: readonly SignedResourceId[];\n\n /** Every id the mirror currently holds, final or not. Delta uses it to tell a reference it\n * must resolve from one already satisfied locally. */\n readonly knownResources: ReadonlySet<SignedResourceId>;\n\n /** Change token from the transaction this request will run in, for a delta walk. Absent\n * means \"send the full tree\", which is also what a token the server cannot use gets. */\n readonly changedSinceToken?: Uint8Array;\n}\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use delta polling when the backend advertises `treeChangedSince:v2`,\n * else backend streaming when it advertises `treeFilter:v2`, else client-side BFS.\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 * - `\"backend-delta\"`: always prefer delta polling, which hands the backend the transaction's\n * change token and takes only what changed since it; if `treeChangedSince:v2` is absent,\n * logs a warning and falls back to the best available path (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** A concrete loading algorithm: a {@link TraversalMode} with `\"auto\"` and any unsupported\n * preference already resolved against the server's capabilities. */\nexport type TreeLoadingAlgorithmName = \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** Resolves a traversal mode into the algorithm a tree will run. A tree calls this once, when\n * it is made, and keeps the answer for its whole life, so the choice (and the fallback warning\n * for a preference the backend cannot serve) happens once rather than on every poll. */\nexport function resolveTreeLoadingAlgorithm(\n mode: TraversalMode,\n capabilities: readonly string[] = [],\n logger?: { warn: (msg: string) => void },\n): TreeLoadingAlgorithmName {\n const streaming = supportsResourceTreeTraversal(capabilities);\n const delta = supportsTreeDelta(capabilities);\n switch (mode) {\n case \"client-bfs\":\n return \"client-bfs\";\n case \"backend-delta\":\n if (delta) return \"backend-delta\";\n (logger ?? console).warn(\n \"traversalMode=backend-delta but backend lacks treeChangedSince:v2 capability; falling back to \" +\n (streaming ? \"backend-streaming\" : \"client-bfs\"),\n );\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n case \"backend-streaming\":\n if (streaming) return \"backend-streaming\";\n (logger ?? console).warn(\n \"traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS\",\n );\n return \"client-bfs\";\n case \"auto\":\n // Delta first: its cost tracks what changed rather than what the tree holds. Streaming\n // is the fallback for a backend that can shape a walk but not date one.\n if (delta) return \"backend-delta\";\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n }\n}\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<\n TreeLoadingRequest,\n \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\" | \"changedSinceToken\"\n > = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n const materialized = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n materialized.add(res.id);\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n for (const root of tree.roots) if (!materialized.has(root)) seedResources.push(root);\n\n return {\n seedResources,\n finalResources,\n roots: [...tree.roots],\n knownResources: materialized,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n changedSinceToken: options.changedSinceToken,\n };\n}\n\nexport type TreeLoadingStat = ResourceUpdateStat & {\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 /** Backend paths: resourceTree() streams consumed. Streaming spends 1, or 2 with a\n * follow-up; delta spends 1 plus one per resolution round. */\n streamRounds: number;\n /** Backend paths: resource frames received. */\n resourceFrames: number;\n /** Streaming path: stopMarker frames received (both skipped and follow-up). */\n stopMarkerFrames: number;\n /** Streaming path: stop markers that were not final locally and triggered a follow-up fetch. */\n stopMarkersFollowUp: number;\n /** Streaming path: frames where the backend stopped traversal (final or traverseWasStopped). */\n traverseWasStoppedCount: number;\n /** BFS path: resource states actually requested from the backend this cycle (after intra-cycle dedup). */\n bfsResourcesRequested: number;\n /** BFS path: requested resources that no longer exist (undefined reply). */\n bfsResourcesNotFound: number;\n /** Delta path: seed ids handed to the backend, summed over every round of the poll.\n * This is what seeding the whole frontier costs on the uplink. */\n deltaSeedsSent: number;\n /** Delta path: extra rounds spent resolving references a delta body pointed at but the\n * response did not carry. */\n deltaResolutionRounds: number;\n /** Delta path: polls that sent a token and got back a response the size of the whole\n * mirror, which is what a refused token looks like from here - rejection is silent, so this\n * is the only tell. Heuristic: a genuinely large change set trips it too. */\n deltaSuspectedFullAnswers: 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 streamRounds: 0,\n resourceFrames: 0,\n stopMarkerFrames: 0,\n stopMarkersFollowUp: 0,\n traverseWasStoppedCount: 0,\n bfsResourcesRequested: 0,\n bfsResourcesNotFound: 0,\n deltaSeedsSent: 0,\n deltaResolutionRounds: 0,\n deltaSuspectedFullAnswers: 0,\n resourcesNew: 0,\n resourcesChanged: 0,\n resourcesUnchanged: 0,\n bytesUnchanged: 0,\n metadataStableChanged: 0,\n bfsRequestsWasted: 0,\n usedStreaming: false,\n fieldsAdded: 0,\n fieldsRemoved: 0,\n fieldsChanged: 0,\n kvChanged: 0,\n readyFlips: 0,\n locksChanged: 0,\n errorsAttached: 0,\n duplicatesResolved: 0,\n resourcesMarkedFinal: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n return `Requests: ${stat.requests}\nTotal time: ${msToHumanReadable(stat.millisSpent)}\nRound-trips: ${stat.roundTrips}\nResources: ${stat.retrievedResources}\nFields: ${stat.retrievedFields}\nKV: ${stat.retrievedKeyValues}\nData Bytes: ${stat.retrievedResourceDataBytes}\nKV Bytes: ${stat.retrievedKeyValueBytes}\nPruned fields: ${stat.prunedFields}\nFinal resources skipped: ${stat.finalResourcesSkipped}\nStop markers skipped: ${stat.stopMarkersSkipped}\nStop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}\nNew resources: ${stat.resourcesNew}\nChanged resources: ${stat.resourcesChanged}\nUnchanged (duplicate re-fetch) resources: ${stat.resourcesUnchanged}\nUnchanged bytes (wasted downlink): ${stat.bytesUnchanged}\nChanged with stable metadata: ${stat.metadataStableChanged}\nBFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}\nUsed streaming: ${stat.usedStreaming}\n[backend] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}\n[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}\n[delta] seeds sent: ${stat.deltaSeedsSent}, resolution rounds: ${stat.deltaResolutionRounds}, suspected full answers: ${stat.deltaSuspectedFullAnswers}`;\n}\n\nexport function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeFilter:v2\");\n}\n\nfunction supportsTreeDelta(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeChangedSince:v2\");\n}\n\nexport function 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 if (stats) stats.bfsResourcesRequested++;\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) {\n if (stats) stats.bfsResourcesNotFound++;\n return undefined;\n }\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 (stats) stats.stopMarkerFrames++;\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n if (stats) stats.stopMarkersFollowUp++;\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (stats) {\n stats.resourceFrames++;\n if (frame.traverseWasStopped) stats.traverseWasStoppedCount++;\n }\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 let { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n }\n\n // Resolve stop-marker seeds by fetching them (see the note below on why the stop rule is not\n // reapplied). Loop in case a fetch still yields stop markers; dedup fetched ids so shared refs\n // or diamonds cannot loop forever (the id set is finite, so the loop terminates). A referenced\n // resource left unloaded would make updateFromResourceData throw \"orphan resource\".\n let pendingSeeds = followUpSeeds;\n const fetchedSeeds = new Set<SignedResourceId>();\n while (pendingSeeds.length > 0) {\n const roundSeeds = pendingSeeds.filter((id) => !fetchedSeeds.has(id));\n if (roundSeeds.length === 0) break;\n for (const id of roundSeeds) fetchedSeeds.add(id);\n\n // No traverseStopRules here on purpose: the seeds are the stop-marked resources, and the\n // stop rule (finality-based) would re-flag them as stop markers instead of loading their\n // state, leaving resources that reference them as orphans. The retry must fetch them plainly.\n const followUpItems = tx.resourceTree(roundSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult, followUpSeeds: nextSeeds } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n // spreading fails due to exceeding stack argument size\n result = result.concat(followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`,\n );\n stats.roundTrips++;\n stats.streamRounds++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n pendingSeeds = nextSeeds;\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 // A tree passes its pinned algorithm here, which resolves to itself. The mode form is\n // kept for callers that run a single load.\n const algorithm = resolveTreeLoadingAlgorithm(mode, capabilities, logger);\n // Both backend paths, not just streaming: state.ts reads this to attribute a BFS-only\n // wasted-fetch counter, and leaving it false for delta reports phantom waste.\n if (stats) stats.usedStreaming = algorithm !== \"client-bfs\";\n\n switch (algorithm) {\n case \"backend-delta\":\n return await loadDeltaTreeState(tx, loadingRequest, stats, logger);\n case \"backend-streaming\":\n return await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger);\n case \"client-bfs\":\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n default:\n throw new Error(`unknown tree loading algorithm: ${algorithm as string}`);\n }\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","loadDeltaTreeState"],"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, ResourceUpdateStat } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\nimport { loadDeltaTreeState } from \"./delta_sync\";\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 * @deprecated prune with {@link changedSinceToken} instead; ignored by the backend under one. */\n readonly traverseStopRules?: Filter;\n\n /** The tree's roots. Delta seeds at these only when the non-final frontier is empty;\n * resourceTree requires at least one seed. */\n readonly roots: readonly SignedResourceId[];\n\n /** Every id the mirror currently holds, final or not. Delta uses it to tell a reference it\n * must resolve from one already satisfied locally. */\n readonly knownResources: ReadonlySet<SignedResourceId>;\n\n /** Change token from the transaction this request will run in, for a delta walk. Absent\n * means \"send the full tree\", which is also what a token the server cannot use gets. */\n readonly changedSinceToken?: Uint8Array;\n}\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use delta polling when the backend advertises `treeChangedSince:v2`,\n * else backend streaming when it advertises `treeFilter:v2`, else client-side BFS.\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 * - `\"backend-delta\"`: always prefer delta polling, which hands the backend the transaction's\n * change token and takes only what changed since it; if `treeChangedSince:v2` is absent,\n * logs a warning and falls back to the best available path (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** A concrete loading algorithm: a {@link TraversalMode} with `\"auto\"` and any unsupported\n * preference already resolved against the server's capabilities. */\nexport type TreeLoadingAlgorithmName = \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** Resolves a traversal mode into the algorithm a tree will run. A tree calls this once, when\n * it is made, and keeps the answer for its whole life, so the choice (and the fallback warning\n * for a preference the backend cannot serve) happens once rather than on every poll. */\nexport function resolveTreeLoadingAlgorithm(\n mode: TraversalMode,\n capabilities: readonly string[] = [],\n logger?: { warn: (msg: string) => void },\n): TreeLoadingAlgorithmName {\n const streaming = supportsResourceTreeTraversal(capabilities);\n const delta = supportsTreeDelta(capabilities);\n switch (mode) {\n case \"client-bfs\":\n return \"client-bfs\";\n case \"backend-delta\":\n if (delta) return \"backend-delta\";\n (logger ?? console).warn(\n \"traversalMode=backend-delta but backend lacks treeChangedSince:v2 capability; falling back to \" +\n (streaming ? \"backend-streaming\" : \"client-bfs\"),\n );\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n case \"backend-streaming\":\n if (streaming) return \"backend-streaming\";\n (logger ?? console).warn(\n \"traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS\",\n );\n return \"client-bfs\";\n case \"auto\":\n // Delta first: its cost tracks what changed rather than what the tree holds. Streaming\n // is the fallback for a backend that can shape a walk but not date one.\n if (delta) return \"backend-delta\";\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n }\n}\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<\n TreeLoadingRequest,\n \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\" | \"changedSinceToken\"\n > = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n const materialized = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n materialized.add(res.id);\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n for (const root of tree.roots) if (!materialized.has(root)) seedResources.push(root);\n\n return {\n seedResources,\n finalResources,\n roots: [...tree.roots],\n knownResources: materialized,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n changedSinceToken: options.changedSinceToken,\n };\n}\n\nexport type TreeLoadingStat = ResourceUpdateStat & {\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 /** Backend paths: resourceTree() streams consumed. Streaming spends 1, or 2 with a\n * follow-up; delta spends 1 plus one per resolution round. */\n streamRounds: number;\n /** Backend paths: resource frames received. */\n resourceFrames: number;\n /** Streaming path: stopMarker frames received (both skipped and follow-up). */\n stopMarkerFrames: number;\n /** Streaming path: stop markers that were not final locally and triggered a follow-up fetch. */\n stopMarkersFollowUp: number;\n /** Streaming path: frames where the backend stopped traversal (final or traverseWasStopped). */\n traverseWasStoppedCount: number;\n /** BFS path: resource states actually requested from the backend this cycle (after intra-cycle dedup). */\n bfsResourcesRequested: number;\n /** BFS path: requested resources that no longer exist (undefined reply). */\n bfsResourcesNotFound: number;\n /** Delta path: seed ids handed to the backend, summed over every round of the poll.\n * This is what seeding the whole frontier costs on the uplink. */\n deltaSeedsSent: number;\n /** Delta path: extra rounds spent resolving references a delta body pointed at but the\n * response did not carry. */\n deltaResolutionRounds: number;\n /** Roots dropped from the mirror because they are absent from the backend. */\n rootsDropped: number;\n /** Delta path: polls that sent a token and got back a response the size of the whole\n * mirror, which is what a refused token looks like from here - rejection is silent, so this\n * is the only tell. Heuristic: a genuinely large change set trips it too. */\n deltaSuspectedFullAnswers: 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 streamRounds: 0,\n resourceFrames: 0,\n stopMarkerFrames: 0,\n stopMarkersFollowUp: 0,\n traverseWasStoppedCount: 0,\n bfsResourcesRequested: 0,\n bfsResourcesNotFound: 0,\n deltaSeedsSent: 0,\n deltaResolutionRounds: 0,\n deltaSuspectedFullAnswers: 0,\n rootsDropped: 0,\n resourcesNew: 0,\n resourcesChanged: 0,\n resourcesUnchanged: 0,\n bytesUnchanged: 0,\n metadataStableChanged: 0,\n bfsRequestsWasted: 0,\n usedStreaming: false,\n fieldsAdded: 0,\n fieldsRemoved: 0,\n fieldsChanged: 0,\n kvChanged: 0,\n readyFlips: 0,\n locksChanged: 0,\n errorsAttached: 0,\n duplicatesResolved: 0,\n resourcesMarkedFinal: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n return `Requests: ${stat.requests}\nTotal time: ${msToHumanReadable(stat.millisSpent)}\nRound-trips: ${stat.roundTrips}\nResources: ${stat.retrievedResources}\nFields: ${stat.retrievedFields}\nKV: ${stat.retrievedKeyValues}\nData Bytes: ${stat.retrievedResourceDataBytes}\nKV Bytes: ${stat.retrievedKeyValueBytes}\nPruned fields: ${stat.prunedFields}\nFinal resources skipped: ${stat.finalResourcesSkipped}\nStop markers skipped: ${stat.stopMarkersSkipped}\nStop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}\nNew resources: ${stat.resourcesNew}\nChanged resources: ${stat.resourcesChanged}\nUnchanged (duplicate re-fetch) resources: ${stat.resourcesUnchanged}\nUnchanged bytes (wasted downlink): ${stat.bytesUnchanged}\nChanged with stable metadata: ${stat.metadataStableChanged}\nBFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}\nUsed streaming: ${stat.usedStreaming}\n[backend] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}\n[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}\n[delta] seeds sent: ${stat.deltaSeedsSent}, resolution rounds: ${stat.deltaResolutionRounds}, suspected full answers: ${stat.deltaSuspectedFullAnswers}`;\n}\n\nexport function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeFilter:v2\");\n}\n\nfunction supportsTreeDelta(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeChangedSince:v2\");\n}\n\nexport function 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 if (stats) stats.bfsResourcesRequested++;\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) {\n if (stats) stats.bfsResourcesNotFound++;\n return undefined;\n }\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 (stats) stats.stopMarkerFrames++;\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n if (stats) stats.stopMarkersFollowUp++;\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (stats) {\n stats.resourceFrames++;\n if (frame.traverseWasStopped) stats.traverseWasStoppedCount++;\n }\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 // The backend's flag as sent. A stopped traversal (children not streamed) is handled\n // through the fields below, not by claiming the backend marked the resource final.\n final: frame.final,\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 let { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n }\n\n // Resolve stop-marker seeds by fetching them (see the note below on why the stop rule is not\n // reapplied). Loop in case a fetch still yields stop markers; dedup fetched ids so shared refs\n // or diamonds cannot loop forever (the id set is finite, so the loop terminates). A referenced\n // resource left unloaded would make updateFromResourceData throw \"orphan resource\".\n let pendingSeeds = followUpSeeds;\n const fetchedSeeds = new Set<SignedResourceId>();\n while (pendingSeeds.length > 0) {\n const roundSeeds = pendingSeeds.filter((id) => !fetchedSeeds.has(id));\n if (roundSeeds.length === 0) break;\n for (const id of roundSeeds) fetchedSeeds.add(id);\n\n // No traverseStopRules here on purpose: the seeds are the stop-marked resources, and the\n // stop rule (finality-based) would re-flag them as stop markers instead of loading their\n // state, leaving resources that reference them as orphans. The retry must fetch them plainly.\n const followUpItems = tx.resourceTree(roundSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult, followUpSeeds: nextSeeds } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n // spreading fails due to exceeding stack argument size\n result = result.concat(followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`,\n );\n stats.roundTrips++;\n stats.streamRounds++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n pendingSeeds = nextSeeds;\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 // A tree passes its pinned algorithm here, which resolves to itself. The mode form is\n // kept for callers that run a single load.\n const algorithm = resolveTreeLoadingAlgorithm(mode, capabilities, logger);\n // Both backend paths, not just streaming: state.ts reads this to attribute a BFS-only\n // wasted-fetch counter, and leaving it false for delta reports phantom waste.\n if (stats) stats.usedStreaming = algorithm !== \"client-bfs\";\n\n switch (algorithm) {\n case \"backend-delta\":\n return await loadDeltaTreeState(tx, loadingRequest, stats, logger);\n case \"backend-streaming\":\n return await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger);\n case \"client-bfs\":\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n default:\n throw new Error(`unknown tree loading algorithm: ${algorithm as string}`);\n }\n } finally {\n if (stats) stats.millisSpent += Date.now() - startTimestamp;\n }\n}\n"],"mappings":"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"}
|
package/dist/sync.d.ts
CHANGED
|
@@ -88,6 +88,8 @@ export type TreeLoadingStat = ResourceUpdateStat & {
|
|
|
88
88
|
/** Delta path: extra rounds spent resolving references a delta body pointed at but the
|
|
89
89
|
* response did not carry. */
|
|
90
90
|
deltaResolutionRounds: number;
|
|
91
|
+
/** Roots dropped from the mirror because they are absent from the backend. */
|
|
92
|
+
rootsDropped: number;
|
|
91
93
|
/** Delta path: polls that sent a token and got back a response the size of the whole
|
|
92
94
|
* mirror, which is what a refused token looks like from here - rejection is silent, so this
|
|
93
95
|
* is the only tell. Heuristic: a genuinely large change set trips it too. */
|
package/dist/sync.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","names":[],"sources":["../src/sync.ts"],"mappings":";;;;YAeY,mBAAmB,UAAU,yBAAyB;iBAEjD;;;;WAIN,eAAe;;;;WAKf,gBAAgB,IAAI;;WAGpB,kBAAkB;;WAGlB,cAAc;;;WAId,oBAAoB;;;WAIpB,gBAAgB;;;WAIhB,gBAAgB,YAAY;;;WAI5B,oBAAoB;;;;;;;;;;;;YAanB;;;YAIA;;;;wBAKI,4BACd,MAAM,eACN,kCACA;EAAW,OAAO;IACjB;;;wBA6Ba,4BACd,MAAM,aACN,UAAS,KACP,qGAGD;YAwBS,kBAAkB;EAC5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;;;EAGA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;EAGA;;;EAGA;;;;EAIA;;wBAGc,0BAA0B;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","names":[],"sources":["../src/sync.ts"],"mappings":";;;;YAeY,mBAAmB,UAAU,yBAAyB;iBAEjD;;;;WAIN,eAAe;;;;WAKf,gBAAgB,IAAI;;WAGpB,kBAAkB;;WAGlB,cAAc;;;WAId,oBAAoB;;;WAIpB,gBAAgB;;;WAIhB,gBAAgB,YAAY;;;WAI5B,oBAAoB;;;;;;;;;;;;YAanB;;;YAIA;;;;wBAKI,4BACd,MAAM,eACN,kCACA;EAAW,OAAO;IACjB;;;wBA6Ba,4BACd,MAAM,aACN,UAAS,KACP,qGAGD;YAwBS,kBAAkB;EAC5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;;EAEA;;;EAGA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;EAGA;;;EAGA;;EAEA;;;;EAIA;;wBAGc,0BAA0B;wBA4C1B,sBAAsB,MAAM;wBAyB5B,8BAA8B;wBAQ9B,wBAAwB,UAAU,sBAAsB,QAAQ;;;;wBA2P1D,cACpB,IAAI,eACJ,gBAAgB,oBAChB,QAAQ,iBACR,kCACA,OAAM,eACN;EAAW,OAAO;EAAsB,QAAQ;IAC/C,QAAQ"}
|
package/dist/sync.js
CHANGED
|
@@ -71,6 +71,7 @@ function initialTreeLoadingStat() {
|
|
|
71
71
|
deltaSeedsSent: 0,
|
|
72
72
|
deltaResolutionRounds: 0,
|
|
73
73
|
deltaSuspectedFullAnswers: 0,
|
|
74
|
+
rootsDropped: 0,
|
|
74
75
|
resourcesNew: 0,
|
|
75
76
|
resourcesChanged: 0,
|
|
76
77
|
resourcesUnchanged: 0,
|
|
@@ -219,7 +220,7 @@ async function processResourceTreeStream(treeItems, finalResources, pruningFunct
|
|
|
219
220
|
resourceReady: frame.resourceReady,
|
|
220
221
|
error: frame.error,
|
|
221
222
|
originalResourceId: frame.originalResourceId,
|
|
222
|
-
final: frame.final
|
|
223
|
+
final: frame.final,
|
|
223
224
|
inputsLocked: frame.inputsLocked,
|
|
224
225
|
outputsLocked: frame.outputsLocked,
|
|
225
226
|
fields: frame.fields,
|
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, ResourceUpdateStat } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\nimport { loadDeltaTreeState } from \"./delta_sync\";\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 * @deprecated prune with {@link changedSinceToken} instead; ignored by the backend under one. */\n readonly traverseStopRules?: Filter;\n\n /** The tree's roots. Delta seeds at these only when the non-final frontier is empty;\n * resourceTree requires at least one seed. */\n readonly roots: readonly SignedResourceId[];\n\n /** Every id the mirror currently holds, final or not. Delta uses it to tell a reference it\n * must resolve from one already satisfied locally. */\n readonly knownResources: ReadonlySet<SignedResourceId>;\n\n /** Change token from the transaction this request will run in, for a delta walk. Absent\n * means \"send the full tree\", which is also what a token the server cannot use gets. */\n readonly changedSinceToken?: Uint8Array;\n}\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use delta polling when the backend advertises `treeChangedSince:v2`,\n * else backend streaming when it advertises `treeFilter:v2`, else client-side BFS.\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 * - `\"backend-delta\"`: always prefer delta polling, which hands the backend the transaction's\n * change token and takes only what changed since it; if `treeChangedSince:v2` is absent,\n * logs a warning and falls back to the best available path (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** A concrete loading algorithm: a {@link TraversalMode} with `\"auto\"` and any unsupported\n * preference already resolved against the server's capabilities. */\nexport type TreeLoadingAlgorithmName = \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** Resolves a traversal mode into the algorithm a tree will run. A tree calls this once, when\n * it is made, and keeps the answer for its whole life, so the choice (and the fallback warning\n * for a preference the backend cannot serve) happens once rather than on every poll. */\nexport function resolveTreeLoadingAlgorithm(\n mode: TraversalMode,\n capabilities: readonly string[] = [],\n logger?: { warn: (msg: string) => void },\n): TreeLoadingAlgorithmName {\n const streaming = supportsResourceTreeTraversal(capabilities);\n const delta = supportsTreeDelta(capabilities);\n switch (mode) {\n case \"client-bfs\":\n return \"client-bfs\";\n case \"backend-delta\":\n if (delta) return \"backend-delta\";\n (logger ?? console).warn(\n \"traversalMode=backend-delta but backend lacks treeChangedSince:v2 capability; falling back to \" +\n (streaming ? \"backend-streaming\" : \"client-bfs\"),\n );\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n case \"backend-streaming\":\n if (streaming) return \"backend-streaming\";\n (logger ?? console).warn(\n \"traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS\",\n );\n return \"client-bfs\";\n case \"auto\":\n // Delta first: its cost tracks what changed rather than what the tree holds. Streaming\n // is the fallback for a backend that can shape a walk but not date one.\n if (delta) return \"backend-delta\";\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n }\n}\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<\n TreeLoadingRequest,\n \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\" | \"changedSinceToken\"\n > = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n const materialized = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n materialized.add(res.id);\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n for (const root of tree.roots) if (!materialized.has(root)) seedResources.push(root);\n\n return {\n seedResources,\n finalResources,\n roots: [...tree.roots],\n knownResources: materialized,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n changedSinceToken: options.changedSinceToken,\n };\n}\n\nexport type TreeLoadingStat = ResourceUpdateStat & {\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 /** Backend paths: resourceTree() streams consumed. Streaming spends 1, or 2 with a\n * follow-up; delta spends 1 plus one per resolution round. */\n streamRounds: number;\n /** Backend paths: resource frames received. */\n resourceFrames: number;\n /** Streaming path: stopMarker frames received (both skipped and follow-up). */\n stopMarkerFrames: number;\n /** Streaming path: stop markers that were not final locally and triggered a follow-up fetch. */\n stopMarkersFollowUp: number;\n /** Streaming path: frames where the backend stopped traversal (final or traverseWasStopped). */\n traverseWasStoppedCount: number;\n /** BFS path: resource states actually requested from the backend this cycle (after intra-cycle dedup). */\n bfsResourcesRequested: number;\n /** BFS path: requested resources that no longer exist (undefined reply). */\n bfsResourcesNotFound: number;\n /** Delta path: seed ids handed to the backend, summed over every round of the poll.\n * This is what seeding the whole frontier costs on the uplink. */\n deltaSeedsSent: number;\n /** Delta path: extra rounds spent resolving references a delta body pointed at but the\n * response did not carry. */\n deltaResolutionRounds: number;\n /** Delta path: polls that sent a token and got back a response the size of the whole\n * mirror, which is what a refused token looks like from here - rejection is silent, so this\n * is the only tell. Heuristic: a genuinely large change set trips it too. */\n deltaSuspectedFullAnswers: 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 streamRounds: 0,\n resourceFrames: 0,\n stopMarkerFrames: 0,\n stopMarkersFollowUp: 0,\n traverseWasStoppedCount: 0,\n bfsResourcesRequested: 0,\n bfsResourcesNotFound: 0,\n deltaSeedsSent: 0,\n deltaResolutionRounds: 0,\n deltaSuspectedFullAnswers: 0,\n resourcesNew: 0,\n resourcesChanged: 0,\n resourcesUnchanged: 0,\n bytesUnchanged: 0,\n metadataStableChanged: 0,\n bfsRequestsWasted: 0,\n usedStreaming: false,\n fieldsAdded: 0,\n fieldsRemoved: 0,\n fieldsChanged: 0,\n kvChanged: 0,\n readyFlips: 0,\n locksChanged: 0,\n errorsAttached: 0,\n duplicatesResolved: 0,\n resourcesMarkedFinal: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n return `Requests: ${stat.requests}\nTotal time: ${msToHumanReadable(stat.millisSpent)}\nRound-trips: ${stat.roundTrips}\nResources: ${stat.retrievedResources}\nFields: ${stat.retrievedFields}\nKV: ${stat.retrievedKeyValues}\nData Bytes: ${stat.retrievedResourceDataBytes}\nKV Bytes: ${stat.retrievedKeyValueBytes}\nPruned fields: ${stat.prunedFields}\nFinal resources skipped: ${stat.finalResourcesSkipped}\nStop markers skipped: ${stat.stopMarkersSkipped}\nStop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}\nNew resources: ${stat.resourcesNew}\nChanged resources: ${stat.resourcesChanged}\nUnchanged (duplicate re-fetch) resources: ${stat.resourcesUnchanged}\nUnchanged bytes (wasted downlink): ${stat.bytesUnchanged}\nChanged with stable metadata: ${stat.metadataStableChanged}\nBFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}\nUsed streaming: ${stat.usedStreaming}\n[backend] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}\n[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}\n[delta] seeds sent: ${stat.deltaSeedsSent}, resolution rounds: ${stat.deltaResolutionRounds}, suspected full answers: ${stat.deltaSuspectedFullAnswers}`;\n}\n\nexport function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeFilter:v2\");\n}\n\nfunction supportsTreeDelta(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeChangedSince:v2\");\n}\n\nexport function 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 if (stats) stats.bfsResourcesRequested++;\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) {\n if (stats) stats.bfsResourcesNotFound++;\n return undefined;\n }\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 (stats) stats.stopMarkerFrames++;\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n if (stats) stats.stopMarkersFollowUp++;\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (stats) {\n stats.resourceFrames++;\n if (frame.traverseWasStopped) stats.traverseWasStoppedCount++;\n }\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 let { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n }\n\n // Resolve stop-marker seeds by fetching them (see the note below on why the stop rule is not\n // reapplied). Loop in case a fetch still yields stop markers; dedup fetched ids so shared refs\n // or diamonds cannot loop forever (the id set is finite, so the loop terminates). A referenced\n // resource left unloaded would make updateFromResourceData throw \"orphan resource\".\n let pendingSeeds = followUpSeeds;\n const fetchedSeeds = new Set<SignedResourceId>();\n while (pendingSeeds.length > 0) {\n const roundSeeds = pendingSeeds.filter((id) => !fetchedSeeds.has(id));\n if (roundSeeds.length === 0) break;\n for (const id of roundSeeds) fetchedSeeds.add(id);\n\n // No traverseStopRules here on purpose: the seeds are the stop-marked resources, and the\n // stop rule (finality-based) would re-flag them as stop markers instead of loading their\n // state, leaving resources that reference them as orphans. The retry must fetch them plainly.\n const followUpItems = tx.resourceTree(roundSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult, followUpSeeds: nextSeeds } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n // spreading fails due to exceeding stack argument size\n result = result.concat(followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`,\n );\n stats.roundTrips++;\n stats.streamRounds++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n pendingSeeds = nextSeeds;\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 // A tree passes its pinned algorithm here, which resolves to itself. The mode form is\n // kept for callers that run a single load.\n const algorithm = resolveTreeLoadingAlgorithm(mode, capabilities, logger);\n // Both backend paths, not just streaming: state.ts reads this to attribute a BFS-only\n // wasted-fetch counter, and leaving it false for delta reports phantom waste.\n if (stats) stats.usedStreaming = algorithm !== \"client-bfs\";\n\n switch (algorithm) {\n case \"backend-delta\":\n return await loadDeltaTreeState(tx, loadingRequest, stats, logger);\n case \"backend-streaming\":\n return await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger);\n case \"client-bfs\":\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n default:\n throw new Error(`unknown tree loading algorithm: ${algorithm as string}`);\n }\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, ResourceUpdateStat } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\nimport { loadDeltaTreeState } from \"./delta_sync\";\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 * @deprecated prune with {@link changedSinceToken} instead; ignored by the backend under one. */\n readonly traverseStopRules?: Filter;\n\n /** The tree's roots. Delta seeds at these only when the non-final frontier is empty;\n * resourceTree requires at least one seed. */\n readonly roots: readonly SignedResourceId[];\n\n /** Every id the mirror currently holds, final or not. Delta uses it to tell a reference it\n * must resolve from one already satisfied locally. */\n readonly knownResources: ReadonlySet<SignedResourceId>;\n\n /** Change token from the transaction this request will run in, for a delta walk. Absent\n * means \"send the full tree\", which is also what a token the server cannot use gets. */\n readonly changedSinceToken?: Uint8Array;\n}\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use delta polling when the backend advertises `treeChangedSince:v2`,\n * else backend streaming when it advertises `treeFilter:v2`, else client-side BFS.\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 * - `\"backend-delta\"`: always prefer delta polling, which hands the backend the transaction's\n * change token and takes only what changed since it; if `treeChangedSince:v2` is absent,\n * logs a warning and falls back to the best available path (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** A concrete loading algorithm: a {@link TraversalMode} with `\"auto\"` and any unsupported\n * preference already resolved against the server's capabilities. */\nexport type TreeLoadingAlgorithmName = \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** Resolves a traversal mode into the algorithm a tree will run. A tree calls this once, when\n * it is made, and keeps the answer for its whole life, so the choice (and the fallback warning\n * for a preference the backend cannot serve) happens once rather than on every poll. */\nexport function resolveTreeLoadingAlgorithm(\n mode: TraversalMode,\n capabilities: readonly string[] = [],\n logger?: { warn: (msg: string) => void },\n): TreeLoadingAlgorithmName {\n const streaming = supportsResourceTreeTraversal(capabilities);\n const delta = supportsTreeDelta(capabilities);\n switch (mode) {\n case \"client-bfs\":\n return \"client-bfs\";\n case \"backend-delta\":\n if (delta) return \"backend-delta\";\n (logger ?? console).warn(\n \"traversalMode=backend-delta but backend lacks treeChangedSince:v2 capability; falling back to \" +\n (streaming ? \"backend-streaming\" : \"client-bfs\"),\n );\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n case \"backend-streaming\":\n if (streaming) return \"backend-streaming\";\n (logger ?? console).warn(\n \"traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS\",\n );\n return \"client-bfs\";\n case \"auto\":\n // Delta first: its cost tracks what changed rather than what the tree holds. Streaming\n // is the fallback for a backend that can shape a walk but not date one.\n if (delta) return \"backend-delta\";\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n }\n}\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<\n TreeLoadingRequest,\n \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\" | \"changedSinceToken\"\n > = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n const materialized = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n materialized.add(res.id);\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n for (const root of tree.roots) if (!materialized.has(root)) seedResources.push(root);\n\n return {\n seedResources,\n finalResources,\n roots: [...tree.roots],\n knownResources: materialized,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n changedSinceToken: options.changedSinceToken,\n };\n}\n\nexport type TreeLoadingStat = ResourceUpdateStat & {\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 /** Backend paths: resourceTree() streams consumed. Streaming spends 1, or 2 with a\n * follow-up; delta spends 1 plus one per resolution round. */\n streamRounds: number;\n /** Backend paths: resource frames received. */\n resourceFrames: number;\n /** Streaming path: stopMarker frames received (both skipped and follow-up). */\n stopMarkerFrames: number;\n /** Streaming path: stop markers that were not final locally and triggered a follow-up fetch. */\n stopMarkersFollowUp: number;\n /** Streaming path: frames where the backend stopped traversal (final or traverseWasStopped). */\n traverseWasStoppedCount: number;\n /** BFS path: resource states actually requested from the backend this cycle (after intra-cycle dedup). */\n bfsResourcesRequested: number;\n /** BFS path: requested resources that no longer exist (undefined reply). */\n bfsResourcesNotFound: number;\n /** Delta path: seed ids handed to the backend, summed over every round of the poll.\n * This is what seeding the whole frontier costs on the uplink. */\n deltaSeedsSent: number;\n /** Delta path: extra rounds spent resolving references a delta body pointed at but the\n * response did not carry. */\n deltaResolutionRounds: number;\n /** Roots dropped from the mirror because they are absent from the backend. */\n rootsDropped: number;\n /** Delta path: polls that sent a token and got back a response the size of the whole\n * mirror, which is what a refused token looks like from here - rejection is silent, so this\n * is the only tell. Heuristic: a genuinely large change set trips it too. */\n deltaSuspectedFullAnswers: 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 streamRounds: 0,\n resourceFrames: 0,\n stopMarkerFrames: 0,\n stopMarkersFollowUp: 0,\n traverseWasStoppedCount: 0,\n bfsResourcesRequested: 0,\n bfsResourcesNotFound: 0,\n deltaSeedsSent: 0,\n deltaResolutionRounds: 0,\n deltaSuspectedFullAnswers: 0,\n rootsDropped: 0,\n resourcesNew: 0,\n resourcesChanged: 0,\n resourcesUnchanged: 0,\n bytesUnchanged: 0,\n metadataStableChanged: 0,\n bfsRequestsWasted: 0,\n usedStreaming: false,\n fieldsAdded: 0,\n fieldsRemoved: 0,\n fieldsChanged: 0,\n kvChanged: 0,\n readyFlips: 0,\n locksChanged: 0,\n errorsAttached: 0,\n duplicatesResolved: 0,\n resourcesMarkedFinal: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n return `Requests: ${stat.requests}\nTotal time: ${msToHumanReadable(stat.millisSpent)}\nRound-trips: ${stat.roundTrips}\nResources: ${stat.retrievedResources}\nFields: ${stat.retrievedFields}\nKV: ${stat.retrievedKeyValues}\nData Bytes: ${stat.retrievedResourceDataBytes}\nKV Bytes: ${stat.retrievedKeyValueBytes}\nPruned fields: ${stat.prunedFields}\nFinal resources skipped: ${stat.finalResourcesSkipped}\nStop markers skipped: ${stat.stopMarkersSkipped}\nStop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}\nNew resources: ${stat.resourcesNew}\nChanged resources: ${stat.resourcesChanged}\nUnchanged (duplicate re-fetch) resources: ${stat.resourcesUnchanged}\nUnchanged bytes (wasted downlink): ${stat.bytesUnchanged}\nChanged with stable metadata: ${stat.metadataStableChanged}\nBFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}\nUsed streaming: ${stat.usedStreaming}\n[backend] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}\n[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}\n[delta] seeds sent: ${stat.deltaSeedsSent}, resolution rounds: ${stat.deltaResolutionRounds}, suspected full answers: ${stat.deltaSuspectedFullAnswers}`;\n}\n\nexport function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeFilter:v2\");\n}\n\nfunction supportsTreeDelta(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeChangedSince:v2\");\n}\n\nexport function 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 if (stats) stats.bfsResourcesRequested++;\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) {\n if (stats) stats.bfsResourcesNotFound++;\n return undefined;\n }\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 (stats) stats.stopMarkerFrames++;\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n if (stats) stats.stopMarkersFollowUp++;\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (stats) {\n stats.resourceFrames++;\n if (frame.traverseWasStopped) stats.traverseWasStoppedCount++;\n }\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 // The backend's flag as sent. A stopped traversal (children not streamed) is handled\n // through the fields below, not by claiming the backend marked the resource final.\n final: frame.final,\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 let { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n }\n\n // Resolve stop-marker seeds by fetching them (see the note below on why the stop rule is not\n // reapplied). Loop in case a fetch still yields stop markers; dedup fetched ids so shared refs\n // or diamonds cannot loop forever (the id set is finite, so the loop terminates). A referenced\n // resource left unloaded would make updateFromResourceData throw \"orphan resource\".\n let pendingSeeds = followUpSeeds;\n const fetchedSeeds = new Set<SignedResourceId>();\n while (pendingSeeds.length > 0) {\n const roundSeeds = pendingSeeds.filter((id) => !fetchedSeeds.has(id));\n if (roundSeeds.length === 0) break;\n for (const id of roundSeeds) fetchedSeeds.add(id);\n\n // No traverseStopRules here on purpose: the seeds are the stop-marked resources, and the\n // stop rule (finality-based) would re-flag them as stop markers instead of loading their\n // state, leaving resources that reference them as orphans. The retry must fetch them plainly.\n const followUpItems = tx.resourceTree(roundSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult, followUpSeeds: nextSeeds } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n // spreading fails due to exceeding stack argument size\n result = result.concat(followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`,\n );\n stats.roundTrips++;\n stats.streamRounds++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n pendingSeeds = nextSeeds;\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 // A tree passes its pinned algorithm here, which resolves to itself. The mode form is\n // kept for callers that run a single load.\n const algorithm = resolveTreeLoadingAlgorithm(mode, capabilities, logger);\n // Both backend paths, not just streaming: state.ts reads this to attribute a BFS-only\n // wasted-fetch counter, and leaving it false for delta reports phantom waste.\n if (stats) stats.usedStreaming = algorithm !== \"client-bfs\";\n\n switch (algorithm) {\n case \"backend-delta\":\n return await loadDeltaTreeState(tx, loadingRequest, stats, logger);\n case \"backend-streaming\":\n return await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger);\n case \"client-bfs\":\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n default:\n throw new Error(`unknown tree loading algorithm: ${algorithm as string}`);\n }\n } finally {\n if (stats) stats.millisSpent += Date.now() - startTimestamp;\n }\n}\n"],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ const require_sync = require("./sync.cjs");
|
|
|
5
5
|
const require_persisted_tree = require("./persisted_tree.cjs");
|
|
6
6
|
let _milaboratories_pl_client = require("@milaboratories/pl-client");
|
|
7
7
|
let _milaboratories_computable = require("@milaboratories/computable");
|
|
8
|
+
let _milaboratories_ts_helpers = require("@milaboratories/ts-helpers");
|
|
8
9
|
let node_timers_promises = require("node:timers/promises");
|
|
9
10
|
node_timers_promises = require_runtime.__toESM(node_timers_promises, 1);
|
|
10
11
|
//#region src/synchronized_tree.ts
|
|
@@ -29,6 +30,16 @@ const RTT_POLL_FACTOR = 2;
|
|
|
29
30
|
* Mirrors `MAX_ADAPTIVE_REQUEST_TIMEOUT` on the deadline side: past this the link is stuck
|
|
30
31
|
* rather than slow, and spacing polls further only delays noticing it recovered. */
|
|
31
32
|
const MAX_RTT_POLL_INTERVAL_MS = 3e4;
|
|
33
|
+
/** Waits between rebuilds after consecutive {@link TreeStateUpdateError}s. The first retry
|
|
34
|
+
* comes after the ordinary floor, so a one-off inconsistency heals as fast as a normal poll;
|
|
35
|
+
* a persistent one settles at one full read per {@link MAX_POLLING_INTERVAL_MS}. */
|
|
36
|
+
const REBUILD_RETRY = {
|
|
37
|
+
type: "exponentialWithMaxDelayBackoff",
|
|
38
|
+
initialDelay: MIN_POLLING_INTERVAL_MS,
|
|
39
|
+
maxDelay: MAX_POLLING_INTERVAL_MS,
|
|
40
|
+
backoffMultiplier: 2,
|
|
41
|
+
jitter: .2
|
|
42
|
+
};
|
|
32
43
|
/** Normalizes the {@link SynchronizedTreeState.init} seed argument — a bare
|
|
33
44
|
* {@link SignedResourceId}, a single {@link TreeSeed}, or an array — to `TreeSeed[]`, so
|
|
34
45
|
* every existing single-root caller is unchanged. */
|
|
@@ -59,7 +70,7 @@ const DISCOVERY_INTERVAL_MS = 3e3;
|
|
|
59
70
|
* `resourcesUnchanged` is excluded by design, since a cycle that only re-fetched unchanged
|
|
60
71
|
* state is exactly the idle case the backoff exists for. */
|
|
61
72
|
function countedChanges(stat) {
|
|
62
|
-
return stat.resourcesNew + stat.resourcesChanged + stat.resourcesMarkedFinal + stat.
|
|
73
|
+
return stat.resourcesNew + stat.resourcesChanged + stat.resourcesMarkedFinal + stat.rootsDropped;
|
|
63
74
|
}
|
|
64
75
|
/** The poll-cadence policy, as a pure function of the last cycle's outcome.
|
|
65
76
|
*
|
|
@@ -111,7 +122,7 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
111
122
|
constructor(pl, seeds, ops, logger) {
|
|
112
123
|
this.pl = pl;
|
|
113
124
|
this.logger = logger;
|
|
114
|
-
const { finalPredicateOverride, pruning, fieldFilter, traverseStopRules, traversalMode, pollingInterval, stopPollingDelay, logStat } = ops;
|
|
125
|
+
const { finalPredicateOverride, pruning, fieldFilter, traverseStopRules, traversalMode, pollingInterval, stopPollingDelay, logStat, rootsNeverFinal } = ops;
|
|
115
126
|
this.pruning = pruning;
|
|
116
127
|
this.fieldFilter = fieldFilter;
|
|
117
128
|
this.traverseStopRules = traverseStopRules;
|
|
@@ -120,7 +131,9 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
120
131
|
logger?.info(`tree loading algorithm: ${this.algorithm} (traversalMode=${this.traversalMode})`);
|
|
121
132
|
this.pollingInterval = pollingInterval;
|
|
122
133
|
this.effectivePollingInterval = pollingInterval;
|
|
123
|
-
|
|
134
|
+
const basePredicate = finalPredicateOverride ?? pl.finalPredicate;
|
|
135
|
+
const explicitRoots = new Set(seeds.flatMap((s) => s.kind === "resource" ? [s.root] : []));
|
|
136
|
+
this.finalPredicate = rootsNeverFinal ? (r) => !explicitRoots.has(r.id) && basePredicate(r) : basePredicate;
|
|
124
137
|
this.logStat = logStat;
|
|
125
138
|
this.explicitRoots = seeds.filter((s) => s.kind === "resource").map((s) => s.root);
|
|
126
139
|
this.sharedSeeds = seeds.filter((s) => s.kind === "shared");
|
|
@@ -284,17 +297,33 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
284
297
|
changedSinceToken: this.deltaToken
|
|
285
298
|
});
|
|
286
299
|
if (request.seedResources.length === 0 && request.finalResources.size === 0) return;
|
|
287
|
-
const
|
|
300
|
+
const checkedRoots = this.state.nonFinalRoots();
|
|
301
|
+
const { data, nextToken, rootsExist } = await this.pl.withReadTx("ReadingTree", async (tx) => {
|
|
288
302
|
const tokenPromise = this.algorithm === "backend-delta" ? tx.getNextSinceToken() : void 0;
|
|
303
|
+
const existence = Promise.all(checkedRoots.map((rid) => tx.resourceExists(rid)));
|
|
304
|
+
existence.catch(() => {});
|
|
289
305
|
return {
|
|
290
306
|
data: await require_sync.loadTreeState(tx, request, stats, this.pl.serverInfo.capabilities ?? [], this.algorithm, this.logger),
|
|
291
|
-
nextToken: await tokenPromise
|
|
307
|
+
nextToken: await tokenPromise,
|
|
308
|
+
rootsExist: await existence
|
|
292
309
|
};
|
|
293
310
|
}, txOps);
|
|
294
311
|
this.state.updateFromResourceData(data, {
|
|
295
312
|
allowOrphanInputs: true,
|
|
296
313
|
stat: stats
|
|
297
314
|
});
|
|
315
|
+
let gone = checkedRoots.filter((_, i) => !rootsExist[i]);
|
|
316
|
+
let dropped = true;
|
|
317
|
+
while (dropped) {
|
|
318
|
+
dropped = false;
|
|
319
|
+
gone = gone.filter((rid) => {
|
|
320
|
+
if (!this.state.dropDeletedRoot(rid)) return true;
|
|
321
|
+
dropped = true;
|
|
322
|
+
if (stats) stats.rootsDropped++;
|
|
323
|
+
this.logger?.warn(`tree root ${(0, _milaboratories_pl_client.resourceIdToString)(rid)} no longer exists; dropped from the tree`);
|
|
324
|
+
return false;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
298
327
|
if (nextToken !== void 0) this.deltaToken = nextToken;
|
|
299
328
|
else if (this.algorithm === "backend-delta") this.demoteFromDelta();
|
|
300
329
|
}
|
|
@@ -351,6 +380,7 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
351
380
|
let stat = require_sync.initialTreeLoadingStat();
|
|
352
381
|
let lastUpdate = Date.now();
|
|
353
382
|
let lastDiscovery = 0;
|
|
383
|
+
let rebuildRetry;
|
|
354
384
|
while (true) {
|
|
355
385
|
if (!this.keepRunning || this.terminated) break;
|
|
356
386
|
let toNotify = void 0;
|
|
@@ -371,18 +401,22 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
371
401
|
this.updatePollingInterval(changed);
|
|
372
402
|
if (this.logStat && this.logger) this.logger.info(`Tree stat (success, after ${Date.now() - lastUpdate}ms): ${JSON.stringify(stat)}`);
|
|
373
403
|
lastUpdate = Date.now();
|
|
404
|
+
rebuildRetry = void 0;
|
|
374
405
|
if (toNotify !== void 0) for (const n of toNotify) n.resolve();
|
|
375
406
|
} catch (e) {
|
|
376
407
|
if (this.logStat && this.logger) this.logger.info(`Tree stat (error, after ${Date.now() - lastUpdate}ms): ${JSON.stringify(stat)}`);
|
|
377
408
|
lastUpdate = Date.now();
|
|
378
409
|
if (toNotify !== void 0) for (const n of toNotify) n.reject(e);
|
|
379
410
|
if (e instanceof require_state.TreeStateUpdateError) {
|
|
380
|
-
|
|
411
|
+
rebuildRetry = rebuildRetry === void 0 ? (0, _milaboratories_ts_helpers.createInfiniteRetryState)(REBUILD_RETRY) : (0, _milaboratories_ts_helpers.nextInfiniteRetryState)(rebuildRetry);
|
|
412
|
+
this.logger?.error(new Error(`tree rebuilt after an update error; next read in ${Math.round(rebuildRetry.nextDelay)}ms`, { cause: e }));
|
|
381
413
|
this.state.invalidateTree("stat update error");
|
|
382
414
|
this.state = new require_state.PlTreeState(this.currentRootSet(), this.finalPredicate);
|
|
383
415
|
this.discardDeltaToken("tree rebuilt after update error");
|
|
384
|
-
|
|
385
|
-
|
|
416
|
+
} else {
|
|
417
|
+
rebuildRetry = void 0;
|
|
418
|
+
this.logger?.warn(e);
|
|
419
|
+
}
|
|
386
420
|
}
|
|
387
421
|
if (!this.keepRunning || this.terminated) break;
|
|
388
422
|
try {
|
|
@@ -393,7 +427,7 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
393
427
|
}
|
|
394
428
|
if (!this.keepRunning || this.terminated) break;
|
|
395
429
|
if (this.scheduledOnNextState.length === 0) {
|
|
396
|
-
const remaining = Math.max(0, this.effectivePollingInterval - MIN_POLLING_INTERVAL_MS);
|
|
430
|
+
const remaining = Math.max(0, (rebuildRetry?.nextDelay ?? this.effectivePollingInterval) - MIN_POLLING_INTERVAL_MS);
|
|
397
431
|
if (remaining > 0) try {
|
|
398
432
|
this.currentLoopDelayInterrupt = new AbortController();
|
|
399
433
|
await node_timers_promises.setTimeout(remaining, void 0, { signal: AbortSignal.any([this.abortController.signal, this.currentLoopDelayInterrupt.signal]) });
|
|
@@ -422,6 +456,9 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
422
456
|
this.keepRunning = false;
|
|
423
457
|
this.terminated = true;
|
|
424
458
|
this.abortController.abort();
|
|
459
|
+
const pending = this.scheduledOnNextState;
|
|
460
|
+
this.scheduledOnNextState = [];
|
|
461
|
+
for (const n of pending) n.reject(/* @__PURE__ */ new Error("tree synchronization is terminated"));
|
|
425
462
|
if (this.currentLoop === void 0) return;
|
|
426
463
|
await this.currentLoop;
|
|
427
464
|
this.state.invalidateTree("synchronization terminated for the tree");
|