@milaboratories/pl-tree 1.13.7 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accessors.d.ts +15 -17
- package/dist/accessors.d.ts.map +1 -1
- package/dist/dump.d.ts +14 -10
- package/dist/dump.d.ts.map +1 -1
- package/dist/index.cjs +7 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/persisted_tree.cjs +465 -0
- package/dist/persisted_tree.cjs.map +1 -0
- package/dist/persisted_tree.d.ts +99 -0
- package/dist/persisted_tree.d.ts.map +1 -0
- package/dist/persisted_tree.js +460 -0
- package/dist/persisted_tree.js.map +1 -0
- package/dist/snapshot.d.ts +12 -14
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/state.cjs +7 -0
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.ts +13 -11
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +7 -0
- package/dist/state.js.map +1 -1
- package/dist/sync.d.ts +26 -19
- package/dist/sync.d.ts.map +1 -1
- package/dist/synchronized_tree.cjs +62 -4
- package/dist/synchronized_tree.cjs.map +1 -1
- package/dist/synchronized_tree.d.ts +64 -17
- package/dist/synchronized_tree.d.ts.map +1 -1
- package/dist/synchronized_tree.js +62 -4
- package/dist/synchronized_tree.js.map +1 -1
- package/dist/traversal_ops.d.ts +12 -10
- package/dist/traversal_ops.d.ts.map +1 -1
- package/dist/value_and_error.d.ts +3 -4
- package/dist/value_and_error.d.ts.map +1 -1
- package/dist/value_or_error.d.ts +1 -2
- package/dist/value_or_error.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +1 -0
- package/src/persisted_tree.test.ts +291 -0
- package/src/persisted_tree.ts +708 -0
- package/src/state.ts +8 -0
- package/src/synchronized_tree.ts +102 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"synchronized_tree.cjs","names":["PlTreeState","PollingComputableHooks","PlTreeEntry","PlTreeRootsEntry","constructTreeLoadingRequest","loadTreeState","initialTreeLoadingStat","TreeStateUpdateError","tp"],"sources":["../src/synchronized_tree.ts"],"sourcesContent":["import { PollingComputableHooks } from \"@milaboratories/computable\";\nimport { PlTreeEntry, PlTreeRootsEntry } from \"./accessors\";\nimport type {\n FinalResourceDataPredicate,\n PlClient,\n ResourceType,\n SignedResourceId,\n TxOps,\n} from \"@milaboratories/pl-client\";\nimport type { Filter } from \"@milaboratories/pl-client\";\nimport {\n isUnauthenticated,\n isTimeoutOrCancelError,\n isUnimplementedError,\n} from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData } from \"./state\";\nimport { PlTreeState, TreeStateUpdateError } from \"./state\";\nimport type { PruningFunction, TraversalMode, TreeLoadingStat } from \"./sync\";\nimport { constructTreeLoadingRequest, initialTreeLoadingStat, loadTreeState } from \"./sync\";\nimport * as tp from \"node:timers/promises\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\n\n/** Hard floor between consecutive tree-refresh calls.\n * Applies even when {@link scheduleOnNextState} has woken the loop early,\n * preventing tight polling loops during rapid state transitions. */\nconst MIN_POLLING_INTERVAL_MS = 100;\n\n/** Ceiling for the adaptive poll interval. Caps how stale an idle tree can get before the\n * next look, and bounds how far the idle backoff can push the interval out. */\nconst MAX_POLLING_INTERVAL_MS = 5_000;\n\n/** Applied to the interval after a cycle that changed nothing. An idle tree walks its\n * interval out towards {@link MAX_POLLING_INTERVAL_MS} instead of re-polling at full rate;\n * the first cycle that changes anything resets it. */\nconst IDLE_BACKOFF_MULTIPLIER = 1.5;\n\n/** The client's measured RTT becomes an interval floor, scaled by this. Every refresh costs\n * at least one round trip, so polling faster than a small multiple of the RTT only queues\n * round-trips the link cannot service. This is what replaces the fixed interval on a\n * high-latency link; on a fast link the configured `pollingInterval` still dominates. */\nconst RTT_POLL_FACTOR = 2;\n\n/** Ceiling for the RTT-derived floor. The estimate is sampled at connect time and never\n * re-sampled, so without a bound one slow ping pins the cadence high for the whole session.\n * Mirrors `MAX_ADAPTIVE_REQUEST_TIMEOUT` on the deadline side: past this the link is stuck\n * rather than slow, and spacing polls further only delays noticing it recovered. */\nconst MAX_RTT_POLL_INTERVAL_MS = 30_000;\n\ntype StatLoggingMode = \"cumulative\" | \"per-request\";\n\nexport type SynchronizedTreeOps = {\n /** Override final predicate from the PlClient */\n finalPredicateOverride?: FinalResourceDataPredicate;\n\n /** Pruning function for legacy fallback path. */\n pruning?: PruningFunction;\n\n /** ResourceTree field filter for modern backend path. */\n fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules for modern backend path. */\n traverseStopRules?: Filter;\n\n /** Interval after last sync to sleep before the next one */\n pollingInterval: number;\n /** For how long to continue polling after the last derived value access */\n stopPollingDelay: number;\n\n /** If one of the values, tree will log stats of each polling request */\n logStat?: StatLoggingMode;\n\n /** Timeout for initial tree loading. If not specified, will use default for RO tx from pl-client. */\n initialTreeLoadingTimeout?: number;\n\n /** Controls which tree-loading path to use. Default `\"auto\"`. */\n traversalMode?: TraversalMode;\n};\n\n/** An explicit resource to serve as a tree root. Several explicit seeds may be passed. */\nexport type ExplicitRootSeed = { kind: \"resource\"; root: SignedResourceId };\n\n/** Discovers, as roots, every resource of this type shared with the current user.\n * Matched against SharedResource.resourceType by NAME (version optional/ignored). The\n * discovered set is DYNAMIC — roots appear/disappear as grants are added/revoked/expire. */\nexport type SharedTypeSeed = { kind: \"shared\"; resourceType: ResourceType };\n\nexport type TreeSeed = ExplicitRootSeed | SharedTypeSeed;\n\n/** Normalizes the {@link SynchronizedTreeState.init} seed argument — a bare\n * {@link SignedResourceId}, a single {@link TreeSeed}, or an array — to `TreeSeed[]`, so\n * every existing single-root caller is unchanged. */\nfunction normalizeSeeds(seeds: SignedResourceId | TreeSeed | TreeSeed[]): TreeSeed[] {\n if (Array.isArray(seeds)) return seeds;\n if (typeof seeds === \"object\" && seeds !== null && \"kind\" in seeds) return [seeds];\n // bare SignedResourceId\n return [{ kind: \"resource\", root: seeds }];\n}\n\n/** How often a tree with shared-type seeds re-polls ListUserResources to reconcile its\n * discovered roots.\n *\n * Discovery (a full ListUserResources stream) is far heavier than an ordinary incremental\n * refresh, so it must NOT run on every refresh tick. This is a wall-clock interval rather\n * than a count of iterations: the refresh cadence is adaptive (it stretches towards\n * {@link MAX_POLLING_INTERVAL_MS} on an idle tree and scales with RTT on a slow link),\n * so a fixed iteration count would let discovery latency drift out with it. Keeping it in\n * milliseconds pins the latency of noticing a new/removed share regardless of cadence, which\n * is what a human-driven share flow cares about.\n *\n * Only trees with shared-type seeds gate on this; {@link discover} is a no-op for single-root\n * and explicit-seed trees (empty `sharedSeeds`), so the value never affects them. */\nconst DISCOVERY_INTERVAL_MS = 3_000;\n\n/** Counters that mean \"this cycle brought something new\". Deliberately not the full change\n * breakdown: those fields are sub-counts of `resourcesChanged` and would double-count.\n * `resourcesUnchanged` is excluded by design, since a cycle that only re-fetched unchanged\n * state is exactly the idle case the backoff exists for. */\nfunction countedChanges(stat: TreeLoadingStat): number {\n return stat.resourcesNew + stat.resourcesChanged + stat.resourcesMarkedFinal;\n}\n\n/** The poll-cadence policy, as a pure function of the last cycle's outcome.\n *\n * `configuredMs` is the tree's static `pollingInterval` and acts as the lower bound, so no\n * link can be polled faster than configured. `rttMs` raises that bound on a slow link.\n * `currentMs` is the interval in force for the cycle that just finished, which is what the\n * idle backoff compounds on. */\nexport function derivePollingInterval(opts: {\n configuredMs: number;\n currentMs: number;\n rttMs: number | undefined;\n changed: boolean;\n}): number {\n const { configuredMs, currentMs, rttMs, changed } = opts;\n\n // The cap applies to the RTT-derived part only, so `configuredMs` stays an absolute lower\n // bound even if it is ever set above the cap.\n const floor =\n rttMs === undefined\n ? configuredMs\n : Math.max(\n configuredMs,\n Math.min(MAX_RTT_POLL_INTERVAL_MS, Math.ceil(rttMs * RTT_POLL_FACTOR)),\n );\n\n const next = changed ? floor : Math.max(floor, currentMs * IDLE_BACKOFF_MULTIPLIER);\n\n // The ceiling never cuts below the floor: a link slower than MAX_POLLING_INTERVAL_MS still\n // gets its RTT-derived spacing rather than being forced to re-poll early.\n return Math.max(floor, Math.min(MAX_POLLING_INTERVAL_MS, next));\n}\n\ntype ScheduledRefresh = {\n resolve: () => void;\n reject: (err: any) => void;\n};\n\nexport class SynchronizedTreeState {\n private readonly finalPredicate: FinalResourceDataPredicate;\n private state: PlTreeState;\n private readonly pollingInterval: number;\n private readonly pruning?: PruningFunction;\n private readonly fieldFilter?: Filter;\n private readonly traverseStopRules?: Filter;\n private readonly traversalMode: TraversalMode;\n private readonly logStat?: StatLoggingMode;\n private readonly hooks: PollingComputableHooks;\n private readonly abortController = new AbortController();\n\n /** Explicit-resource seeds: fixed roots, present from construction. */\n private readonly explicitRoots: SignedResourceId[];\n /** Shared-type seeds (discovered roots), if any. */\n private readonly sharedSeeds: SharedTypeSeed[];\n /** Roots discovered for shared-type seeds on the last discovery poll. */\n private discoveredRoots: SignedResourceId[] = [];\n\n private constructor(\n private readonly pl: PlClient,\n seeds: TreeSeed[],\n ops: SynchronizedTreeOps,\n private readonly logger?: MiLogger,\n ) {\n const {\n finalPredicateOverride,\n pruning,\n fieldFilter,\n traverseStopRules,\n traversalMode,\n pollingInterval,\n stopPollingDelay,\n logStat,\n } = ops;\n this.pruning = pruning;\n this.fieldFilter = fieldFilter;\n this.traverseStopRules = traverseStopRules;\n this.traversalMode = traversalMode ?? \"auto\";\n this.pollingInterval = pollingInterval;\n this.effectivePollingInterval = pollingInterval;\n this.finalPredicate = finalPredicateOverride ?? pl.finalPredicate;\n this.logStat = logStat;\n\n this.explicitRoots = seeds\n .filter((s): s is ExplicitRootSeed => s.kind === \"resource\")\n .map((s) => s.root);\n this.sharedSeeds = seeds.filter((s): s is SharedTypeSeed => s.kind === \"shared\");\n\n this.state = new PlTreeState(this.currentRootSet(), this.finalPredicate);\n this.hooks = new PollingComputableHooks(\n () => this.startUpdating(),\n () => this.stopUpdating(),\n { stopDebounce: stopPollingDelay },\n (resolve, reject) => this.scheduleOnNextState(resolve, reject),\n );\n }\n\n /** The current protected root set: explicit roots plus the latest discovered roots. */\n private currentRootSet(): Set<SignedResourceId> {\n return new Set([...this.explicitRoots, ...this.discoveredRoots]);\n }\n\n /** Resolves the single root for the backward-compatible single-root accessors, throwing\n * if the tree does not have exactly one root (guards legacy callers against multi-root). */\n private soleRoot(): SignedResourceId {\n const roots = this.currentRootSet();\n if (roots.size !== 1)\n throw new Error(\n `single-root accessor used on a tree with ${roots.size} roots; use rootsEntry() instead`,\n );\n return roots.values().next().value!;\n }\n\n /** @deprecated use \"entry\" instead */\n public accessor(rid?: SignedResourceId): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return this.entry(rid);\n }\n\n /** Backward-compatible single-root entry. With no `rid` it returns the sole root's entry\n * and THROWS if the tree has zero or more than one root. An explicit `rid` addresses any\n * resource in the heap, as today. */\n public entry(rid?: SignedResourceId): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeEntry(\n { treeProvider: () => this.state, hooks: this.hooks },\n rid ?? this.soleRoot(),\n );\n }\n\n /** Reactive provider for the current root SET. Reading it inside a Computable tracks the\n * set as a dependency, so the Computable recomputes when discovered roots appear/disappear. */\n public rootsEntry(): PlTreeRootsEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeRootsEntry({ treeProvider: () => this.state, hooks: this.hooks });\n }\n\n /** Can be used to externally kick off the synchronization polling loop, and\n * await for the first synchronization to happen. */\n public async refreshState(): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n await this.hooks.refreshState();\n }\n\n private currentLoopDelayInterrupt: AbortController | undefined = undefined;\n private scheduledOnNextState: ScheduledRefresh[] = [];\n\n /** Interval actually used for the current wait. Starts at the configured `pollingInterval`\n * and is re-derived after every cycle by {@link updatePollingInterval}. */\n private effectivePollingInterval: number;\n\n /** Re-derives {@link effectivePollingInterval} after a cycle.\n *\n * Two independent effects. The floor scales with the client's measured RTT, so a\n * high-latency link stops queueing round-trips it cannot service. On top of that, a cycle\n * that changed nothing multiplies the interval out towards\n * {@link MAX_POLLING_INTERVAL_MS}, while any change snaps it straight back to the floor so\n * an active tree stays responsive. */\n private updatePollingInterval(changed: boolean): void {\n this.effectivePollingInterval = derivePollingInterval({\n configuredMs: this.pollingInterval,\n currentMs: this.effectivePollingInterval,\n rttMs: this.pl.rttEstimateMs,\n changed,\n });\n }\n\n /** Called from computable hooks when external observer asks for state refresh */\n private scheduleOnNextState(resolve: () => void, reject: (err: any) => void): void {\n if (this.terminated) reject(new Error(\"tree synchronization is terminated\"));\n else {\n this.scheduledOnNextState.push({ resolve, reject });\n // Someone is waiting on fresh state, so this tree is not idle after all: drop any\n // accumulated backoff, otherwise the cycles right after a nudge stay slow. Routed\n // through the policy rather than assigning the configured value directly, so the RTT\n // floor survives the reset. Assigning it raw would poll a high-latency link faster than\n // it can answer, and the interval would stay there until the next cycle that completes:\n // the error path never reaches updatePollingInterval, so a failing nudged refresh would\n // keep retrying at the un-floored rate.\n this.updatePollingInterval(true);\n if (this.currentLoopDelayInterrupt) {\n this.currentLoopDelayInterrupt.abort();\n this.currentLoopDelayInterrupt = undefined;\n }\n }\n }\n\n /** Called from observer */\n private startUpdating(): void {\n if (this.terminated) return;\n this.keepRunning = true;\n if (this.currentLoop === undefined) this.currentLoop = this.mainLoop();\n }\n\n /** Called from observer */\n private stopUpdating(): void {\n this.keepRunning = false;\n }\n\n /** If true, main loop will continue polling pl state. */\n private keepRunning = false;\n /** Actual state of main loop. */\n private currentLoop: Promise<void> | undefined = undefined;\n\n /** Executed from the main loop, and initialization procedure. */\n private async refresh(stats?: TreeLoadingStat, txOps?: TxOps): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n try {\n await this.loadAndApply(stats, txOps);\n } catch (e) {\n // Discovery-tree self-heal: a discovered root whose grant was revoked/expired fails the whole\n // ResourceTree poll with Unauthenticated. Re-discover (drops dead roots) and retry once. This is\n // self-discriminating — a genuinely dead session also fails discover()'s own call, so real auth\n // loss still propagates. Only for discovery trees; explicit-root trees propagate as-is.\n if (this.sharedSeeds.length > 0 && isUnauthenticated(e)) {\n this.logger?.warn(\n \"discovery tree: Unauthenticated on ResourceTree (likely revoked/expired root); re-discovering and retrying\",\n );\n await this.discover();\n await this.loadAndApply(stats, txOps);\n } else throw e;\n }\n }\n\n private async loadAndApply(stats?: TreeLoadingStat, txOps?: TxOps): Promise<void> {\n const request = constructTreeLoadingRequest(this.state, {\n pruningFunction: this.pruning,\n fieldFilter: this.fieldFilter,\n traverseStopRules: this.traverseStopRules,\n });\n // A shared-type-seed tree with no currently-discovered roots is legitimately empty:\n // there is nothing to traverse, and tx.resourceTree([]) would throw \"at least one seed\n // must be provided\". Skip the backend load and leave the (empty) state as-is — discovery\n // adds roots later via setRoots(), which schedules the next refresh. Explicit-root trees\n // never hit this (their root set is non-empty by construction).\n if (request.seedResources.length === 0 && request.finalResources.size === 0) return;\n const data = await this.pl.withReadTx(\n \"ReadingTree\",\n async (tx) => {\n return await loadTreeState(\n tx,\n request,\n stats,\n this.pl.serverInfo.capabilities ?? [],\n this.traversalMode,\n this.logger,\n );\n },\n txOps,\n );\n this.state.updateFromResourceData(data, { allowOrphanInputs: true, stat: stats });\n }\n\n /** Discovery sync for shared-type seeds: re-polls `ListUserResources` (gRPC-only) and\n * reconciles the discovered root set against the heap. A longer poll result adds roots; a\n * shorter one removes them (grant revoked/expired) — the removed roots' subtrees cascade\n * to collection via the ordinary refcount GC ({@link PlTreeState.setRoots}). No-op when the\n * tree has no shared-type seeds, or silently no-op on a REST client where `ListUserResources`\n * is unavailable. */\n private async discover(): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n if (this.sharedSeeds.length === 0) return;\n\n const discovered = new Set<SignedResourceId>();\n for (const seed of this.sharedSeeds) {\n let ids: SignedResourceId[];\n try {\n // match by name only (permissive; ignores version, so it survives schema bumps)\n ids = await this.pl.userResources.listSharedResourcesByType(seed.resourceType.name);\n } catch (e: unknown) {\n if (isUnimplementedError(e)) continue;\n throw e;\n }\n for (const id of ids) discovered.add(id);\n }\n\n this.discoveredRoots = [...discovered];\n this.state.setRoots(this.currentRootSet());\n }\n\n /** If true this tree state is permanently terminaed. */\n private terminated = false;\n\n private async mainLoop() {\n // Always collected, even when not logging: the change counters drive the idle backoff\n // below. Counter bumps are cheap next to the round trip they describe.\n let stat = initialTreeLoadingStat();\n\n let lastUpdate = Date.now();\n\n // paces the discovery poll for shared-type seeds; 0 forces discovery on the first pass.\n let lastDiscovery = 0;\n\n while (true) {\n if (!this.keepRunning || this.terminated) break;\n\n // saving those who want to be notified about new state here\n // because those who will be added during the tree retrieval\n // should be notified only on the next round\n let toNotify: ScheduledRefresh[] | undefined = undefined;\n if (this.scheduledOnNextState.length > 0) {\n toNotify = this.scheduledOnNextState;\n this.scheduledOnNextState = [];\n }\n\n try {\n // resetting stats if we were asked to collect non-cumulative stats\n if (this.logStat === \"per-request\") stat = initialTreeLoadingStat();\n\n // discovery sync for shared-type seeds: reconcile the discovered root set before\n // refreshing, so newly discovered roots are materialized in this same iteration.\n if (this.sharedSeeds.length > 0 && Date.now() - lastDiscovery >= DISCOVERY_INTERVAL_MS) {\n await this.discover();\n lastDiscovery = Date.now();\n }\n\n // Change counters before the refresh, so the delta tells us whether this single cycle\n // brought anything new. Works in both stat modes: \"per-request\" resets to 0 above.\n const changesBefore = countedChanges(stat);\n\n // actual tree synchronization\n await this.refresh(stat);\n\n this.updatePollingInterval(countedChanges(stat) > changesBefore);\n\n // logging stats if we were asked to\n if (this.logStat && this.logger)\n this.logger.info(\n `Tree stat (success, after ${Date.now() - lastUpdate}ms): ${JSON.stringify(stat)}`,\n );\n lastUpdate = Date.now();\n\n // notifying that we got new state\n if (toNotify !== undefined) for (const n of toNotify) n.resolve();\n } catch (e: any) {\n // logging stats if we were asked to (even if error occured)\n if (this.logStat && this.logger)\n this.logger.info(\n `Tree stat (error, after ${Date.now() - lastUpdate}ms): ${JSON.stringify(stat)}`,\n );\n lastUpdate = Date.now();\n\n // notifying that we failed to refresh the state\n if (toNotify !== undefined) for (const n of toNotify) n.reject(e);\n\n // catching tree update errors, as they may leave our tree in inconsistent state\n if (e instanceof TreeStateUpdateError) {\n // important error logging, this should never happen\n this.logger?.error(e);\n\n // marking everybody who used previous state as changed\n this.state.invalidateTree(\"stat update error\");\n // creating new tree with the full current root set (re-discovered on next iteration)\n this.state = new PlTreeState(this.currentRootSet(), this.finalPredicate);\n\n // scheduling state update without delay\n continue;\n\n // unfortunately external observer may still see tree in its default\n // empty state, though this is best we can do in this exceptional\n // situation, and hope on caching layers inside computables to present\n // some stale state until we reconstruct the tree again\n } else this.logger?.warn(e);\n }\n\n if (!this.keepRunning || this.terminated) break;\n\n // Phase 1: mandatory floor — always wait at least MIN_POLLING_INTERVAL_MS.\n // Not interruptible by scheduleOnNextState; only termination aborts it.\n try {\n await tp.setTimeout(MIN_POLLING_INTERVAL_MS, undefined, {\n signal: this.abortController.signal,\n });\n } catch (e: unknown) {\n if (!isTimeoutOrCancelError(e)) throw new Error(\"Unexpected error\", { cause: e });\n if (this.abortController.signal.aborted) break;\n }\n\n if (!this.keepRunning || this.terminated) break;\n\n // Phase 2: optional remainder up to pollingInterval — interruptible by\n // scheduleOnNextState so that an external nudge wakes the loop promptly.\n if (this.scheduledOnNextState.length === 0) {\n const remaining = Math.max(0, this.effectivePollingInterval - MIN_POLLING_INTERVAL_MS);\n if (remaining > 0) {\n try {\n this.currentLoopDelayInterrupt = new AbortController();\n await tp.setTimeout(remaining, undefined, {\n signal: AbortSignal.any([\n this.abortController.signal,\n this.currentLoopDelayInterrupt.signal,\n ]),\n });\n } catch (e: unknown) {\n if (!isTimeoutOrCancelError(e)) throw new Error(\"Unexpected error\", { cause: e });\n if (this.abortController.signal.aborted) break;\n // Otherwise it was just the loop delay interrupt (scheduleOnNextState),\n // continue to the next iteration\n } finally {\n this.currentLoopDelayInterrupt = undefined;\n }\n }\n }\n }\n\n // reset only as a very last line\n this.currentLoop = undefined;\n }\n\n /**\n * Dumps the current state of the tree.\n * @returns An array of ExtendedResourceData objects representing the current state of the tree.\n */\n public dumpState(): ExtendedResourceData[] {\n return this.state.dumpState();\n }\n\n /**\n * Terminates the internal loop, and permanently destoys all internal state, so\n * all computables using this state will resolve to errors.\n * */\n public async terminate(): Promise<void> {\n this.keepRunning = false;\n this.terminated = true;\n this.abortController.abort();\n\n if (this.currentLoop === undefined) return;\n await this.currentLoop;\n\n this.state.invalidateTree(\"synchronization terminated for the tree\");\n }\n\n /** @deprecated */\n public async awaitSyncLoopTermination(): Promise<void> {\n if (this.currentLoop === undefined) return;\n await this.currentLoop;\n }\n\n /**\n * Initializes a synchronized tree from one or more seeds.\n *\n * @param seeds a bare {@link SignedResourceId} (the original single-root contract), a\n * single {@link TreeSeed}, or an array of seeds. Bare ids and explicit-resource seeds\n * become roots immediately; shared-type seeds discover their roots via `ListUserResources`.\n */\n public static async init(\n pl: PlClient,\n seeds: SignedResourceId | TreeSeed | TreeSeed[],\n ops: SynchronizedTreeOps,\n logger?: MiLogger,\n ) {\n const tree = new SynchronizedTreeState(pl, normalizeSeeds(seeds), ops, logger);\n\n const stat = ops.logStat ? initialTreeLoadingStat() : undefined;\n\n let ok = false;\n\n try {\n // resolve shared-type seeds before the first refresh so discovered roots load now\n await tree.discover();\n await tree.refresh(stat, {\n timeout: ops.initialTreeLoadingTimeout,\n });\n ok = true;\n } finally {\n // logging stats if we were asked to (even if error occured)\n if (stat && logger)\n logger.info(\n `Tree stat (initial load, ${ok ? \"success\" : \"failure\"}): ${JSON.stringify(stat)}`,\n );\n }\n\n return tree;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"synchronized_tree.cjs","names":["PlTreeState","PollingComputableHooks","captureTreeState","restoreTreeState","PlTreeEntry","PlTreeRootsEntry","constructTreeLoadingRequest","loadTreeState","initialTreeLoadingStat","TreeStateUpdateError","tp"],"sources":["../src/synchronized_tree.ts"],"sourcesContent":["import { PollingComputableHooks } from \"@milaboratories/computable\";\nimport { PlTreeEntry, PlTreeRootsEntry } from \"./accessors\";\nimport type {\n FinalResourceDataPredicate,\n PlClient,\n ResourceSignature,\n ResourceType,\n SignedResourceId,\n TxOps,\n} from \"@milaboratories/pl-client\";\nimport type { Filter } from \"@milaboratories/pl-client\";\nimport {\n isUnauthenticated,\n isTimeoutOrCancelError,\n isUnimplementedError,\n} from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData } from \"./state\";\nimport { PlTreeState, TreeStateUpdateError } from \"./state\";\nimport type { PruningFunction, TraversalMode, TreeLoadingStat } from \"./sync\";\nimport { constructTreeLoadingRequest, initialTreeLoadingStat, loadTreeState } from \"./sync\";\nimport type { PersistedTree } from \"./persisted_tree\";\nimport { captureTreeState, restoreTreeState } from \"./persisted_tree\";\nimport * as tp from \"node:timers/promises\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\n\n/** Hard floor between consecutive tree-refresh calls.\n * Applies even when {@link scheduleOnNextState} has woken the loop early,\n * preventing tight polling loops during rapid state transitions. */\nconst MIN_POLLING_INTERVAL_MS = 100;\n\n/** Ceiling for the adaptive poll interval. Caps how stale an idle tree can get before the\n * next look, and bounds how far the idle backoff can push the interval out. */\nconst MAX_POLLING_INTERVAL_MS = 5_000;\n\n/** Applied to the interval after a cycle that changed nothing. An idle tree walks its\n * interval out towards {@link MAX_POLLING_INTERVAL_MS} instead of re-polling at full rate;\n * the first cycle that changes anything resets it. */\nconst IDLE_BACKOFF_MULTIPLIER = 1.5;\n\n/** The client's measured RTT becomes an interval floor, scaled by this. Every refresh costs\n * at least one round trip, so polling faster than a small multiple of the RTT only queues\n * round-trips the link cannot service. This is what replaces the fixed interval on a\n * high-latency link; on a fast link the configured `pollingInterval` still dominates. */\nconst RTT_POLL_FACTOR = 2;\n\n/** Ceiling for the RTT-derived floor. The estimate is sampled at connect time and never\n * re-sampled, so without a bound one slow ping pins the cadence high for the whole session.\n * Mirrors `MAX_ADAPTIVE_REQUEST_TIMEOUT` on the deadline side: past this the link is stuck\n * rather than slow, and spacing polls further only delays noticing it recovered. */\nconst MAX_RTT_POLL_INTERVAL_MS = 30_000;\n\ntype StatLoggingMode = \"cumulative\" | \"per-request\";\n\nexport type SynchronizedTreeOps = {\n /** Override final predicate from the PlClient */\n finalPredicateOverride?: FinalResourceDataPredicate;\n\n /** Pruning function for legacy fallback path. */\n pruning?: PruningFunction;\n\n /** ResourceTree field filter for modern backend path. */\n fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules for modern backend path. */\n traverseStopRules?: Filter;\n\n /** Interval after last sync to sleep before the next one */\n pollingInterval: number;\n /** For how long to continue polling after the last derived value access */\n stopPollingDelay: number;\n\n /** If one of the values, tree will log stats of each polling request */\n logStat?: StatLoggingMode;\n\n /** Timeout for initial tree loading. If not specified, will use default for RO tx from pl-client. */\n initialTreeLoadingTimeout?: number;\n\n /** Controls which tree-loading path to use. Default `\"auto\"`. */\n traversalMode?: TraversalMode;\n\n /** A previously persisted mirror to seed the tree with, before its first refresh, so that\n * refresh transfers only what changed while the tree was gone.\n *\n * A snapshot that cannot be applied, or does not belong to this tree, is logged and dropped,\n * leaving an ordinary cold open.\n *\n * A snapshot that applies but whose ids are dead is NOT handled here: its resources become\n * this tree's seeds, so the first refresh fails and {@link init} rejects, where a cold open\n * would have succeeded. Establishing that the signatures are still live is the caller's job\n * (see {@link PersistedTree.witness}), as is deciding what to do when the first refresh is\n * refused anyway. Ignored for trees with shared-type seeds, which rediscover their roots. */\n restoreFrom?: PersistedTree;\n};\n\n/** An explicit resource to serve as a tree root. Several explicit seeds may be passed. */\nexport type ExplicitRootSeed = { kind: \"resource\"; root: SignedResourceId };\n\n/** Discovers, as roots, every resource of this type shared with the current user.\n * Matched against SharedResource.resourceType by NAME (version optional/ignored). The\n * discovered set is DYNAMIC — roots appear/disappear as grants are added/revoked/expire. */\nexport type SharedTypeSeed = { kind: \"shared\"; resourceType: ResourceType };\n\nexport type TreeSeed = ExplicitRootSeed | SharedTypeSeed;\n\n/** Normalizes the {@link SynchronizedTreeState.init} seed argument — a bare\n * {@link SignedResourceId}, a single {@link TreeSeed}, or an array — to `TreeSeed[]`, so\n * every existing single-root caller is unchanged. */\nfunction normalizeSeeds(seeds: SignedResourceId | TreeSeed | TreeSeed[]): TreeSeed[] {\n if (Array.isArray(seeds)) return seeds;\n if (typeof seeds === \"object\" && seeds !== null && \"kind\" in seeds) return [seeds];\n // bare SignedResourceId\n return [{ kind: \"resource\", root: seeds }];\n}\n\n/** How often a tree with shared-type seeds re-polls ListUserResources to reconcile its\n * discovered roots.\n *\n * Discovery (a full ListUserResources stream) is far heavier than an ordinary incremental\n * refresh, so it must NOT run on every refresh tick. This is a wall-clock interval rather\n * than a count of iterations: the refresh cadence is adaptive (it stretches towards\n * {@link MAX_POLLING_INTERVAL_MS} on an idle tree and scales with RTT on a slow link),\n * so a fixed iteration count would let discovery latency drift out with it. Keeping it in\n * milliseconds pins the latency of noticing a new/removed share regardless of cadence, which\n * is what a human-driven share flow cares about.\n *\n * Only trees with shared-type seeds gate on this; {@link discover} is a no-op for single-root\n * and explicit-seed trees (empty `sharedSeeds`), so the value never affects them. */\nconst DISCOVERY_INTERVAL_MS = 3_000;\n\n/** Counters that mean \"this cycle brought something new\". Deliberately not the full change\n * breakdown: those fields are sub-counts of `resourcesChanged` and would double-count.\n * `resourcesUnchanged` is excluded by design, since a cycle that only re-fetched unchanged\n * state is exactly the idle case the backoff exists for. */\nfunction countedChanges(stat: TreeLoadingStat): number {\n // `fieldsRemoved` is included despite being a per-field count, because it is the one change\n // that never shows up in `resourcesChanged`: the removed-dynamic-field branch in\n // `updateFromResourceData` does not set its `changed` flag, so a cycle that only dropped a\n // field (and garbage-collected whatever it pointed at) otherwise reads as an idle cycle.\n // That double-counts a resource that both changed and lost a field, which is harmless here:\n // every caller compares this against an earlier value rather than reading it as a total.\n return stat.resourcesNew + stat.resourcesChanged + stat.resourcesMarkedFinal + stat.fieldsRemoved;\n}\n\n/** The poll-cadence policy, as a pure function of the last cycle's outcome.\n *\n * `configuredMs` is the tree's static `pollingInterval` and acts as the lower bound, so no\n * link can be polled faster than configured. `rttMs` raises that bound on a slow link.\n * `currentMs` is the interval in force for the cycle that just finished, which is what the\n * idle backoff compounds on. */\nexport function derivePollingInterval(opts: {\n configuredMs: number;\n currentMs: number;\n rttMs: number | undefined;\n changed: boolean;\n}): number {\n const { configuredMs, currentMs, rttMs, changed } = opts;\n\n // The cap applies to the RTT-derived part only, so `configuredMs` stays an absolute lower\n // bound even if it is ever set above the cap.\n const floor =\n rttMs === undefined\n ? configuredMs\n : Math.max(\n configuredMs,\n Math.min(MAX_RTT_POLL_INTERVAL_MS, Math.ceil(rttMs * RTT_POLL_FACTOR)),\n );\n\n const next = changed ? floor : Math.max(floor, currentMs * IDLE_BACKOFF_MULTIPLIER);\n\n // The ceiling never cuts below the floor: a link slower than MAX_POLLING_INTERVAL_MS still\n // gets its RTT-derived spacing rather than being forced to re-poll early.\n return Math.max(floor, Math.min(MAX_POLLING_INTERVAL_MS, next));\n}\n\ntype ScheduledRefresh = {\n resolve: () => void;\n reject: (err: any) => void;\n};\n\nexport class SynchronizedTreeState {\n private readonly finalPredicate: FinalResourceDataPredicate;\n private state: PlTreeState;\n private readonly pollingInterval: number;\n private readonly pruning?: PruningFunction;\n private readonly fieldFilter?: Filter;\n private readonly traverseStopRules?: Filter;\n private readonly traversalMode: TraversalMode;\n private readonly logStat?: StatLoggingMode;\n private readonly hooks: PollingComputableHooks;\n private readonly abortController = new AbortController();\n\n /** Explicit-resource seeds: fixed roots, present from construction. */\n private readonly explicitRoots: SignedResourceId[];\n /** Shared-type seeds (discovered roots), if any. */\n private readonly sharedSeeds: SharedTypeSeed[];\n /** Roots discovered for shared-type seeds on the last discovery poll. */\n private discoveredRoots: SignedResourceId[] = [];\n\n /** Bumped once per refresh cycle that brought something new: a resource appeared, changed,\n * or became final. Lets a holder tell whether the tree has moved since it last persisted\n * it, without diffing state. Read through {@link changeGeneration}. */\n private changeGenerationCounter = 0;\n\n /** Whether a snapshot was actually applied. Read through\n * {@link wasRestoredFromSnapshot}. */\n private restoredFromSnapshot = false;\n\n private constructor(\n private readonly pl: PlClient,\n seeds: TreeSeed[],\n ops: SynchronizedTreeOps,\n private readonly logger?: MiLogger,\n ) {\n const {\n finalPredicateOverride,\n pruning,\n fieldFilter,\n traverseStopRules,\n traversalMode,\n pollingInterval,\n stopPollingDelay,\n logStat,\n } = ops;\n this.pruning = pruning;\n this.fieldFilter = fieldFilter;\n this.traverseStopRules = traverseStopRules;\n this.traversalMode = traversalMode ?? \"auto\";\n this.pollingInterval = pollingInterval;\n this.effectivePollingInterval = pollingInterval;\n this.finalPredicate = finalPredicateOverride ?? pl.finalPredicate;\n this.logStat = logStat;\n\n this.explicitRoots = seeds\n .filter((s): s is ExplicitRootSeed => s.kind === \"resource\")\n .map((s) => s.root);\n this.sharedSeeds = seeds.filter((s): s is SharedTypeSeed => s.kind === \"shared\");\n\n this.state = new PlTreeState(this.currentRootSet(), this.finalPredicate);\n this.hooks = new PollingComputableHooks(\n () => this.startUpdating(),\n () => this.stopUpdating(),\n { stopDebounce: stopPollingDelay },\n (resolve, reject) => this.scheduleOnNextState(resolve, reject),\n );\n }\n\n /** The current protected root set: explicit roots plus the latest discovered roots. */\n private currentRootSet(): Set<SignedResourceId> {\n return new Set([...this.explicitRoots, ...this.discoveredRoots]);\n }\n\n /** How many refresh cycles brought something new. Only ever increases. Equal values at two\n * points in time mean nothing was added, changed or settled in between, which is what makes\n * a periodic snapshot write skippable on an idle tree. */\n public get changeGeneration(): number {\n return this.changeGenerationCounter;\n }\n\n /** True only if a snapshot was actually applied as this tree's initial state. A snapshot can\n * be supplied and still be refused (wrong roots, or state the update call will not accept),\n * in which case this stays false and the tree started empty like any other. Passing\n * `restoreFrom` is therefore not evidence of a warm start; this is. */\n public get wasRestoredFromSnapshot(): boolean {\n return this.restoredFromSnapshot;\n }\n\n /** Captures the current mirror for persistence.\n *\n * Must be called before {@link terminate}: terminating invalidates the tree, and capturing\n * an invalidated tree is refused rather than silently written. */\n public capture(witness: ResourceSignature): PersistedTree {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return captureTreeState(this.state, witness);\n }\n\n /** Installs a snapshot as this tree's state. Returns false if the snapshot was refused, in\n * which case the tree is left as it was and the open proceeds cold.\n *\n * Only meaningful before the first refresh, which is why it is private and driven from\n * {@link init}: replacing the state of a running tree would strand its observers. */\n private restore(snapshot: PersistedTree): boolean {\n if (this.sharedSeeds.length > 0) {\n this.logger?.warn(\"ignoring tree snapshot: trees with shared-type seeds are not restored\");\n return false;\n }\n\n const roots = this.currentRootSet();\n const snapshotRoots = new Set(snapshot.roots);\n if (snapshotRoots.size !== roots.size || ![...snapshotRoots].every((r) => roots.has(r))) {\n // A snapshot addressed to a different root is a mis-keyed file, not a stale one.\n this.logger?.warn(\"ignoring tree snapshot: its roots are not this tree's roots\");\n return false;\n }\n\n const restored = restoreTreeState(snapshot, this.finalPredicate, {\n roots,\n logger: this.logger,\n });\n if (restored === undefined) return false;\n\n this.state = restored;\n this.restoredFromSnapshot = true;\n return true;\n }\n\n /** Resolves the single root for the backward-compatible single-root accessors, throwing\n * if the tree does not have exactly one root (guards legacy callers against multi-root). */\n private soleRoot(): SignedResourceId {\n const roots = this.currentRootSet();\n if (roots.size !== 1)\n throw new Error(\n `single-root accessor used on a tree with ${roots.size} roots; use rootsEntry() instead`,\n );\n return roots.values().next().value!;\n }\n\n /** @deprecated use \"entry\" instead */\n public accessor(rid?: SignedResourceId): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return this.entry(rid);\n }\n\n /** Backward-compatible single-root entry. With no `rid` it returns the sole root's entry\n * and THROWS if the tree has zero or more than one root. An explicit `rid` addresses any\n * resource in the heap, as today. */\n public entry(rid?: SignedResourceId): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeEntry(\n { treeProvider: () => this.state, hooks: this.hooks },\n rid ?? this.soleRoot(),\n );\n }\n\n /** Reactive provider for the current root SET. Reading it inside a Computable tracks the\n * set as a dependency, so the Computable recomputes when discovered roots appear/disappear. */\n public rootsEntry(): PlTreeRootsEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeRootsEntry({ treeProvider: () => this.state, hooks: this.hooks });\n }\n\n /** Can be used to externally kick off the synchronization polling loop, and\n * await for the first synchronization to happen. */\n public async refreshState(): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n await this.hooks.refreshState();\n }\n\n private currentLoopDelayInterrupt: AbortController | undefined = undefined;\n private scheduledOnNextState: ScheduledRefresh[] = [];\n\n /** Interval actually used for the current wait. Starts at the configured `pollingInterval`\n * and is re-derived after every cycle by {@link updatePollingInterval}. */\n private effectivePollingInterval: number;\n\n /** Re-derives {@link effectivePollingInterval} after a cycle.\n *\n * Two independent effects. The floor scales with the client's measured RTT, so a\n * high-latency link stops queueing round-trips it cannot service. On top of that, a cycle\n * that changed nothing multiplies the interval out towards\n * {@link MAX_POLLING_INTERVAL_MS}, while any change snaps it straight back to the floor so\n * an active tree stays responsive. */\n private updatePollingInterval(changed: boolean): void {\n this.effectivePollingInterval = derivePollingInterval({\n configuredMs: this.pollingInterval,\n currentMs: this.effectivePollingInterval,\n rttMs: this.pl.rttEstimateMs,\n changed,\n });\n }\n\n /** Called from computable hooks when external observer asks for state refresh */\n private scheduleOnNextState(resolve: () => void, reject: (err: any) => void): void {\n if (this.terminated) reject(new Error(\"tree synchronization is terminated\"));\n else {\n this.scheduledOnNextState.push({ resolve, reject });\n // Someone is waiting on fresh state, so this tree is not idle after all: drop any\n // accumulated backoff, otherwise the cycles right after a nudge stay slow. Routed\n // through the policy rather than assigning the configured value directly, so the RTT\n // floor survives the reset. Assigning it raw would poll a high-latency link faster than\n // it can answer, and the interval would stay there until the next cycle that completes:\n // the error path never reaches updatePollingInterval, so a failing nudged refresh would\n // keep retrying at the un-floored rate.\n this.updatePollingInterval(true);\n if (this.currentLoopDelayInterrupt) {\n this.currentLoopDelayInterrupt.abort();\n this.currentLoopDelayInterrupt = undefined;\n }\n }\n }\n\n /** Called from observer */\n private startUpdating(): void {\n if (this.terminated) return;\n this.keepRunning = true;\n if (this.currentLoop === undefined) this.currentLoop = this.mainLoop();\n }\n\n /** Called from observer */\n private stopUpdating(): void {\n this.keepRunning = false;\n }\n\n /** If true, main loop will continue polling pl state. */\n private keepRunning = false;\n /** Actual state of main loop. */\n private currentLoop: Promise<void> | undefined = undefined;\n\n /** Executed from the main loop, and initialization procedure. */\n private async refresh(stats?: TreeLoadingStat, txOps?: TxOps): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n try {\n await this.loadAndApply(stats, txOps);\n } catch (e) {\n // Discovery-tree self-heal: a discovered root whose grant was revoked/expired fails the whole\n // ResourceTree poll with Unauthenticated. Re-discover (drops dead roots) and retry once. This is\n // self-discriminating — a genuinely dead session also fails discover()'s own call, so real auth\n // loss still propagates. Only for discovery trees; explicit-root trees propagate as-is.\n if (this.sharedSeeds.length > 0 && isUnauthenticated(e)) {\n this.logger?.warn(\n \"discovery tree: Unauthenticated on ResourceTree (likely revoked/expired root); re-discovering and retrying\",\n );\n await this.discover();\n await this.loadAndApply(stats, txOps);\n } else throw e;\n }\n }\n\n private async loadAndApply(stats?: TreeLoadingStat, txOps?: TxOps): Promise<void> {\n const request = constructTreeLoadingRequest(this.state, {\n pruningFunction: this.pruning,\n fieldFilter: this.fieldFilter,\n traverseStopRules: this.traverseStopRules,\n });\n // A shared-type-seed tree with no currently-discovered roots is legitimately empty:\n // there is nothing to traverse, and tx.resourceTree([]) would throw \"at least one seed\n // must be provided\". Skip the backend load and leave the (empty) state as-is — discovery\n // adds roots later via setRoots(), which schedules the next refresh. Explicit-root trees\n // never hit this (their root set is non-empty by construction).\n if (request.seedResources.length === 0 && request.finalResources.size === 0) return;\n const data = await this.pl.withReadTx(\n \"ReadingTree\",\n async (tx) => {\n return await loadTreeState(\n tx,\n request,\n stats,\n this.pl.serverInfo.capabilities ?? [],\n this.traversalMode,\n this.logger,\n );\n },\n txOps,\n );\n this.state.updateFromResourceData(data, { allowOrphanInputs: true, stat: stats });\n }\n\n /** Discovery sync for shared-type seeds: re-polls `ListUserResources` (gRPC-only) and\n * reconciles the discovered root set against the heap. A longer poll result adds roots; a\n * shorter one removes them (grant revoked/expired) — the removed roots' subtrees cascade\n * to collection via the ordinary refcount GC ({@link PlTreeState.setRoots}). No-op when the\n * tree has no shared-type seeds, or silently no-op on a REST client where `ListUserResources`\n * is unavailable. */\n private async discover(): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n if (this.sharedSeeds.length === 0) return;\n\n const discovered = new Set<SignedResourceId>();\n for (const seed of this.sharedSeeds) {\n let ids: SignedResourceId[];\n try {\n // match by name only (permissive; ignores version, so it survives schema bumps)\n ids = await this.pl.userResources.listSharedResourcesByType(seed.resourceType.name);\n } catch (e: unknown) {\n if (isUnimplementedError(e)) continue;\n throw e;\n }\n for (const id of ids) discovered.add(id);\n }\n\n this.discoveredRoots = [...discovered];\n this.state.setRoots(this.currentRootSet());\n }\n\n /** If true this tree state is permanently terminaed. */\n private terminated = false;\n\n private async mainLoop() {\n // Always collected, even when not logging: the change counters drive the idle backoff\n // below. Counter bumps are cheap next to the round trip they describe.\n let stat = initialTreeLoadingStat();\n\n let lastUpdate = Date.now();\n\n // paces the discovery poll for shared-type seeds; 0 forces discovery on the first pass.\n let lastDiscovery = 0;\n\n while (true) {\n if (!this.keepRunning || this.terminated) break;\n\n // saving those who want to be notified about new state here\n // because those who will be added during the tree retrieval\n // should be notified only on the next round\n let toNotify: ScheduledRefresh[] | undefined = undefined;\n if (this.scheduledOnNextState.length > 0) {\n toNotify = this.scheduledOnNextState;\n this.scheduledOnNextState = [];\n }\n\n try {\n // resetting stats if we were asked to collect non-cumulative stats\n if (this.logStat === \"per-request\") stat = initialTreeLoadingStat();\n\n // discovery sync for shared-type seeds: reconcile the discovered root set before\n // refreshing, so newly discovered roots are materialized in this same iteration.\n if (this.sharedSeeds.length > 0 && Date.now() - lastDiscovery >= DISCOVERY_INTERVAL_MS) {\n await this.discover();\n lastDiscovery = Date.now();\n }\n\n // Change counters before the refresh, so the delta tells us whether this single cycle\n // brought anything new. Works in both stat modes: \"per-request\" resets to 0 above.\n const changesBefore = countedChanges(stat);\n\n // actual tree synchronization\n await this.refresh(stat);\n\n const changed = countedChanges(stat) > changesBefore;\n if (changed) this.changeGenerationCounter++;\n this.updatePollingInterval(changed);\n\n // logging stats if we were asked to\n if (this.logStat && this.logger)\n this.logger.info(\n `Tree stat (success, after ${Date.now() - lastUpdate}ms): ${JSON.stringify(stat)}`,\n );\n lastUpdate = Date.now();\n\n // notifying that we got new state\n if (toNotify !== undefined) for (const n of toNotify) n.resolve();\n } catch (e: any) {\n // logging stats if we were asked to (even if error occured)\n if (this.logStat && this.logger)\n this.logger.info(\n `Tree stat (error, after ${Date.now() - lastUpdate}ms): ${JSON.stringify(stat)}`,\n );\n lastUpdate = Date.now();\n\n // notifying that we failed to refresh the state\n if (toNotify !== undefined) for (const n of toNotify) n.reject(e);\n\n // catching tree update errors, as they may leave our tree in inconsistent state\n if (e instanceof TreeStateUpdateError) {\n // important error logging, this should never happen\n this.logger?.error(e);\n\n // marking everybody who used previous state as changed\n this.state.invalidateTree(\"stat update error\");\n // creating new tree with the full current root set (re-discovered on next iteration)\n this.state = new PlTreeState(this.currentRootSet(), this.finalPredicate);\n\n // scheduling state update without delay\n continue;\n\n // unfortunately external observer may still see tree in its default\n // empty state, though this is best we can do in this exceptional\n // situation, and hope on caching layers inside computables to present\n // some stale state until we reconstruct the tree again\n } else this.logger?.warn(e);\n }\n\n if (!this.keepRunning || this.terminated) break;\n\n // Phase 1: mandatory floor — always wait at least MIN_POLLING_INTERVAL_MS.\n // Not interruptible by scheduleOnNextState; only termination aborts it.\n try {\n await tp.setTimeout(MIN_POLLING_INTERVAL_MS, undefined, {\n signal: this.abortController.signal,\n });\n } catch (e: unknown) {\n if (!isTimeoutOrCancelError(e)) throw new Error(\"Unexpected error\", { cause: e });\n if (this.abortController.signal.aborted) break;\n }\n\n if (!this.keepRunning || this.terminated) break;\n\n // Phase 2: optional remainder up to pollingInterval — interruptible by\n // scheduleOnNextState so that an external nudge wakes the loop promptly.\n if (this.scheduledOnNextState.length === 0) {\n const remaining = Math.max(0, this.effectivePollingInterval - MIN_POLLING_INTERVAL_MS);\n if (remaining > 0) {\n try {\n this.currentLoopDelayInterrupt = new AbortController();\n await tp.setTimeout(remaining, undefined, {\n signal: AbortSignal.any([\n this.abortController.signal,\n this.currentLoopDelayInterrupt.signal,\n ]),\n });\n } catch (e: unknown) {\n if (!isTimeoutOrCancelError(e)) throw new Error(\"Unexpected error\", { cause: e });\n if (this.abortController.signal.aborted) break;\n // Otherwise it was just the loop delay interrupt (scheduleOnNextState),\n // continue to the next iteration\n } finally {\n this.currentLoopDelayInterrupt = undefined;\n }\n }\n }\n }\n\n // reset only as a very last line\n this.currentLoop = undefined;\n }\n\n /**\n * Dumps the current state of the tree.\n * @returns An array of ExtendedResourceData objects representing the current state of the tree.\n */\n public dumpState(): ExtendedResourceData[] {\n return this.state.dumpState();\n }\n\n /**\n * Terminates the internal loop, and permanently destoys all internal state, so\n * all computables using this state will resolve to errors.\n * */\n public async terminate(): Promise<void> {\n this.keepRunning = false;\n this.terminated = true;\n this.abortController.abort();\n\n if (this.currentLoop === undefined) return;\n await this.currentLoop;\n\n this.state.invalidateTree(\"synchronization terminated for the tree\");\n }\n\n /** @deprecated */\n public async awaitSyncLoopTermination(): Promise<void> {\n if (this.currentLoop === undefined) return;\n await this.currentLoop;\n }\n\n /**\n * Initializes a synchronized tree from one or more seeds.\n *\n * @param seeds a bare {@link SignedResourceId} (the original single-root contract), a\n * single {@link TreeSeed}, or an array of seeds. Bare ids and explicit-resource seeds\n * become roots immediately; shared-type seeds discover their roots via `ListUserResources`.\n */\n public static async init(\n pl: PlClient,\n seeds: SignedResourceId | TreeSeed | TreeSeed[],\n ops: SynchronizedTreeOps,\n logger?: MiLogger,\n ) {\n const tree = new SynchronizedTreeState(pl, normalizeSeeds(seeds), ops, logger);\n\n // Seed from the snapshot before the first refresh, so that refresh is the one that\n // transfers only what changed. A refused snapshot leaves an ordinary cold open.\n const restored = ops.restoreFrom !== undefined && tree.restore(ops.restoreFrom);\n\n // Always collected, even when not logging: the initial load's change count is what seeds\n // the change generation, so a holder can tell a populated tree from an untouched one.\n const stat = initialTreeLoadingStat();\n\n let ok = false;\n\n try {\n // resolve shared-type seeds before the first refresh so discovered roots load now\n await tree.discover();\n await tree.refresh(stat, {\n timeout: ops.initialTreeLoadingTimeout,\n });\n ok = true;\n } finally {\n if (countedChanges(stat) > 0) tree.changeGenerationCounter++;\n\n // logging stats if we were asked to (even if error occured)\n if (ops.logStat && logger)\n logger.info(\n `Tree stat (initial load, ${ok ? \"success\" : \"failure\"}, ${\n restored ? \"restored from snapshot\" : \"cold\"\n }): ${JSON.stringify(stat)}`,\n );\n }\n\n return tree;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AA4BA,MAAM,0BAA0B;;;AAIhC,MAAM,0BAA0B;;;;AAKhC,MAAM,0BAA0B;;;;;AAMhC,MAAM,kBAAkB;;;;;AAMxB,MAAM,2BAA2B;;;;AA0DjC,SAAS,eAAe,OAA6D;CACnF,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CACjC,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,OAAO,OAAO,CAAC,KAAK;CAEjF,OAAO,CAAC;EAAE,MAAM;EAAY,MAAM;CAAM,CAAC;AAC3C;;;;;;;;;;;;;;AAeA,MAAM,wBAAwB;;;;;AAM9B,SAAS,eAAe,MAA+B;CAOrD,OAAO,KAAK,eAAe,KAAK,mBAAmB,KAAK,uBAAuB,KAAK;AACtF;;;;;;;AAQA,SAAgB,sBAAsB,MAK3B;CACT,MAAM,EAAE,cAAc,WAAW,OAAO,YAAY;CAIpD,MAAM,QACJ,UAAU,KAAA,IACN,eACA,KAAK,IACH,cACA,KAAK,IAAI,0BAA0B,KAAK,KAAK,QAAQ,eAAe,CAAC,CACvE;CAEN,MAAM,OAAO,UAAU,QAAQ,KAAK,IAAI,OAAO,YAAY,uBAAuB;CAIlF,OAAO,KAAK,IAAI,OAAO,KAAK,IAAI,yBAAyB,IAAI,CAAC;AAChE;AAOA,IAAa,wBAAb,MAAa,sBAAsB;CA6Bd;CAGA;CA/BnB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,kBAAmC,IAAI,gBAAgB;;CAGvD;;CAEA;;CAEA,kBAA8C,CAAC;;;;CAK/C,0BAAkC;;;CAIlC,uBAA+B;CAE/B,YACE,IACA,OACA,KACA,QACA;EAJiB,KAAA,KAAA;EAGA,KAAA,SAAA;EAEjB,MAAM,EACJ,wBACA,SACA,aACA,mBACA,eACA,iBACA,kBACA,YACE;EACJ,KAAK,UAAU;EACf,KAAK,cAAc;EACnB,KAAK,oBAAoB;EACzB,KAAK,gBAAgB,iBAAiB;EACtC,KAAK,kBAAkB;EACvB,KAAK,2BAA2B;EAChC,KAAK,iBAAiB,0BAA0B,GAAG;EACnD,KAAK,UAAU;EAEf,KAAK,gBAAgB,MAClB,QAAQ,MAA6B,EAAE,SAAS,UAAU,CAAC,CAC3D,KAAK,MAAM,EAAE,IAAI;EACpB,KAAK,cAAc,MAAM,QAAQ,MAA2B,EAAE,SAAS,QAAQ;EAE/E,KAAK,QAAQ,IAAIA,cAAAA,YAAY,KAAK,eAAe,GAAG,KAAK,cAAc;EACvE,KAAK,QAAQ,IAAIC,2BAAAA,6BACT,KAAK,cAAc,SACnB,KAAK,aAAa,GACxB,EAAE,cAAc,iBAAiB,IAChC,SAAS,WAAW,KAAK,oBAAoB,SAAS,MAAM,CAC/D;CACF;;CAGA,iBAAgD;EAC9C,uBAAO,IAAI,IAAI,CAAC,GAAG,KAAK,eAAe,GAAG,KAAK,eAAe,CAAC;CACjE;;;;CAKA,IAAW,mBAA2B;EACpC,OAAO,KAAK;CACd;;;;;CAMA,IAAW,0BAAmC;EAC5C,OAAO,KAAK;CACd;;;;;CAMA,QAAe,SAA2C;EACxD,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,OAAOC,uBAAAA,iBAAiB,KAAK,OAAO,OAAO;CAC7C;;;;;;CAOA,QAAgB,UAAkC;EAChD,IAAI,KAAK,YAAY,SAAS,GAAG;GAC/B,KAAK,QAAQ,KAAK,uEAAuE;GACzF,OAAO;EACT;EAEA,MAAM,QAAQ,KAAK,eAAe;EAClC,MAAM,gBAAgB,IAAI,IAAI,SAAS,KAAK;EAC5C,IAAI,cAAc,SAAS,MAAM,QAAQ,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,OAAO,MAAM,MAAM,IAAI,CAAC,CAAC,GAAG;GAEvF,KAAK,QAAQ,KAAK,6DAA6D;GAC/E,OAAO;EACT;EAEA,MAAM,WAAWC,uBAAAA,iBAAiB,UAAU,KAAK,gBAAgB;GAC/D;GACA,QAAQ,KAAK;EACf,CAAC;EACD,IAAI,aAAa,KAAA,GAAW,OAAO;EAEnC,KAAK,QAAQ;EACb,KAAK,uBAAuB;EAC5B,OAAO;CACT;;;CAIA,WAAqC;EACnC,MAAM,QAAQ,KAAK,eAAe;EAClC,IAAI,MAAM,SAAS,GACjB,MAAM,IAAI,MACR,4CAA4C,MAAM,KAAK,iCACzD;EACF,OAAO,MAAM,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC;CAC/B;;CAGA,SAAgB,KAAqC;EACnD,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,OAAO,KAAK,MAAM,GAAG;CACvB;;;;CAKA,MAAa,KAAqC;EAChD,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,OAAO,IAAIC,kBAAAA,YACT;GAAE,oBAAoB,KAAK;GAAO,OAAO,KAAK;EAAM,GACpD,OAAO,KAAK,SAAS,CACvB;CACF;;;CAIA,aAAsC;EACpC,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,OAAO,IAAIC,kBAAAA,iBAAiB;GAAE,oBAAoB,KAAK;GAAO,OAAO,KAAK;EAAM,CAAC;CACnF;;;CAIA,MAAa,eAA8B;EACzC,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,MAAM,KAAK,MAAM,aAAa;CAChC;CAEA,4BAAiE,KAAA;CACjE,uBAAmD,CAAC;;;CAIpD;;;;;;;;CASA,sBAA8B,SAAwB;EACpD,KAAK,2BAA2B,sBAAsB;GACpD,cAAc,KAAK;GACnB,WAAW,KAAK;GAChB,OAAO,KAAK,GAAG;GACf;EACF,CAAC;CACH;;CAGA,oBAA4B,SAAqB,QAAkC;EACjF,IAAI,KAAK,YAAY,uBAAO,IAAI,MAAM,oCAAoC,CAAC;OACtE;GACH,KAAK,qBAAqB,KAAK;IAAE;IAAS;GAAO,CAAC;GAQlD,KAAK,sBAAsB,IAAI;GAC/B,IAAI,KAAK,2BAA2B;IAClC,KAAK,0BAA0B,MAAM;IACrC,KAAK,4BAA4B,KAAA;GACnC;EACF;CACF;;CAGA,gBAA8B;EAC5B,IAAI,KAAK,YAAY;EACrB,KAAK,cAAc;EACnB,IAAI,KAAK,gBAAgB,KAAA,GAAW,KAAK,cAAc,KAAK,SAAS;CACvE;;CAGA,eAA6B;EAC3B,KAAK,cAAc;CACrB;;CAGA,cAAsB;;CAEtB,cAAiD,KAAA;;CAGjD,MAAc,QAAQ,OAAyB,OAA8B;EAC3E,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,IAAI;GACF,MAAM,KAAK,aAAa,OAAO,KAAK;EACtC,SAAS,GAAG;GAKV,IAAI,KAAK,YAAY,SAAS,MAAA,GAAA,0BAAA,kBAAA,CAAuB,CAAC,GAAG;IACvD,KAAK,QAAQ,KACX,4GACF;IACA,MAAM,KAAK,SAAS;IACpB,MAAM,KAAK,aAAa,OAAO,KAAK;GACtC,OAAO,MAAM;EACf;CACF;CAEA,MAAc,aAAa,OAAyB,OAA8B;EAChF,MAAM,UAAUC,aAAAA,4BAA4B,KAAK,OAAO;GACtD,iBAAiB,KAAK;GACtB,aAAa,KAAK;GAClB,mBAAmB,KAAK;EAC1B,CAAC;EAMD,IAAI,QAAQ,cAAc,WAAW,KAAK,QAAQ,eAAe,SAAS,GAAG;EAC7E,MAAM,OAAO,MAAM,KAAK,GAAG,WACzB,eACA,OAAO,OAAO;GACZ,OAAO,MAAMC,aAAAA,cACX,IACA,SACA,OACA,KAAK,GAAG,WAAW,gBAAgB,CAAC,GACpC,KAAK,eACL,KAAK,MACP;EACF,GACA,KACF;EACA,KAAK,MAAM,uBAAuB,MAAM;GAAE,mBAAmB;GAAM,MAAM;EAAM,CAAC;CAClF;;;;;;;CAQA,MAAc,WAA0B;EACtC,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,IAAI,KAAK,YAAY,WAAW,GAAG;EAEnC,MAAM,6BAAa,IAAI,IAAsB;EAC7C,KAAK,MAAM,QAAQ,KAAK,aAAa;GACnC,IAAI;GACJ,IAAI;IAEF,MAAM,MAAM,KAAK,GAAG,cAAc,0BAA0B,KAAK,aAAa,IAAI;GACpF,SAAS,GAAY;IACnB,KAAA,GAAA,0BAAA,qBAAA,CAAyB,CAAC,GAAG;IAC7B,MAAM;GACR;GACA,KAAK,MAAM,MAAM,KAAK,WAAW,IAAI,EAAE;EACzC;EAEA,KAAK,kBAAkB,CAAC,GAAG,UAAU;EACrC,KAAK,MAAM,SAAS,KAAK,eAAe,CAAC;CAC3C;;CAGA,aAAqB;CAErB,MAAc,WAAW;EAGvB,IAAI,OAAOC,aAAAA,uBAAuB;EAElC,IAAI,aAAa,KAAK,IAAI;EAG1B,IAAI,gBAAgB;EAEpB,OAAO,MAAM;GACX,IAAI,CAAC,KAAK,eAAe,KAAK,YAAY;GAK1C,IAAI,WAA2C,KAAA;GAC/C,IAAI,KAAK,qBAAqB,SAAS,GAAG;IACxC,WAAW,KAAK;IAChB,KAAK,uBAAuB,CAAC;GAC/B;GAEA,IAAI;IAEF,IAAI,KAAK,YAAY,eAAe,OAAOA,aAAAA,uBAAuB;IAIlE,IAAI,KAAK,YAAY,SAAS,KAAK,KAAK,IAAI,IAAI,iBAAiB,uBAAuB;KACtF,MAAM,KAAK,SAAS;KACpB,gBAAgB,KAAK,IAAI;IAC3B;IAIA,MAAM,gBAAgB,eAAe,IAAI;IAGzC,MAAM,KAAK,QAAQ,IAAI;IAEvB,MAAM,UAAU,eAAe,IAAI,IAAI;IACvC,IAAI,SAAS,KAAK;IAClB,KAAK,sBAAsB,OAAO;IAGlC,IAAI,KAAK,WAAW,KAAK,QACvB,KAAK,OAAO,KACV,6BAA6B,KAAK,IAAI,IAAI,WAAW,OAAO,KAAK,UAAU,IAAI,GACjF;IACF,aAAa,KAAK,IAAI;IAGtB,IAAI,aAAa,KAAA,GAAW,KAAK,MAAM,KAAK,UAAU,EAAE,QAAQ;GAClE,SAAS,GAAQ;IAEf,IAAI,KAAK,WAAW,KAAK,QACvB,KAAK,OAAO,KACV,2BAA2B,KAAK,IAAI,IAAI,WAAW,OAAO,KAAK,UAAU,IAAI,GAC/E;IACF,aAAa,KAAK,IAAI;IAGtB,IAAI,aAAa,KAAA,GAAW,KAAK,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;IAGhE,IAAI,aAAaC,cAAAA,sBAAsB;KAErC,KAAK,QAAQ,MAAM,CAAC;KAGpB,KAAK,MAAM,eAAe,mBAAmB;KAE7C,KAAK,QAAQ,IAAIT,cAAAA,YAAY,KAAK,eAAe,GAAG,KAAK,cAAc;KAGvE;IAMF,OAAO,KAAK,QAAQ,KAAK,CAAC;GAC5B;GAEA,IAAI,CAAC,KAAK,eAAe,KAAK,YAAY;GAI1C,IAAI;IACF,MAAMU,qBAAG,WAAW,yBAAyB,KAAA,GAAW,EACtD,QAAQ,KAAK,gBAAgB,OAC/B,CAAC;GACH,SAAS,GAAY;IACnB,IAAI,EAAA,GAAA,0BAAA,uBAAA,CAAwB,CAAC,GAAG,MAAM,IAAI,MAAM,oBAAoB,EAAE,OAAO,EAAE,CAAC;IAChF,IAAI,KAAK,gBAAgB,OAAO,SAAS;GAC3C;GAEA,IAAI,CAAC,KAAK,eAAe,KAAK,YAAY;GAI1C,IAAI,KAAK,qBAAqB,WAAW,GAAG;IAC1C,MAAM,YAAY,KAAK,IAAI,GAAG,KAAK,2BAA2B,uBAAuB;IACrF,IAAI,YAAY,GACd,IAAI;KACF,KAAK,4BAA4B,IAAI,gBAAgB;KACrD,MAAMA,qBAAG,WAAW,WAAW,KAAA,GAAW,EACxC,QAAQ,YAAY,IAAI,CACtB,KAAK,gBAAgB,QACrB,KAAK,0BAA0B,MACjC,CAAC,EACH,CAAC;IACH,SAAS,GAAY;KACnB,IAAI,EAAA,GAAA,0BAAA,uBAAA,CAAwB,CAAC,GAAG,MAAM,IAAI,MAAM,oBAAoB,EAAE,OAAO,EAAE,CAAC;KAChF,IAAI,KAAK,gBAAgB,OAAO,SAAS;IAG3C,UAAU;KACR,KAAK,4BAA4B,KAAA;IACnC;GAEJ;EACF;EAGA,KAAK,cAAc,KAAA;CACrB;;;;;CAMA,YAA2C;EACzC,OAAO,KAAK,MAAM,UAAU;CAC9B;;;;;CAMA,MAAa,YAA2B;EACtC,KAAK,cAAc;EACnB,KAAK,aAAa;EAClB,KAAK,gBAAgB,MAAM;EAE3B,IAAI,KAAK,gBAAgB,KAAA,GAAW;EACpC,MAAM,KAAK;EAEX,KAAK,MAAM,eAAe,yCAAyC;CACrE;;CAGA,MAAa,2BAA0C;EACrD,IAAI,KAAK,gBAAgB,KAAA,GAAW;EACpC,MAAM,KAAK;CACb;;;;;;;;CASA,aAAoB,KAClB,IACA,OACA,KACA,QACA;EACA,MAAM,OAAO,IAAI,sBAAsB,IAAI,eAAe,KAAK,GAAG,KAAK,MAAM;EAI7E,MAAM,WAAW,IAAI,gBAAgB,KAAA,KAAa,KAAK,QAAQ,IAAI,WAAW;EAI9E,MAAM,OAAOF,aAAAA,uBAAuB;EAEpC,IAAI,KAAK;EAET,IAAI;GAEF,MAAM,KAAK,SAAS;GACpB,MAAM,KAAK,QAAQ,MAAM,EACvB,SAAS,IAAI,0BACf,CAAC;GACD,KAAK;EACP,UAAU;GACR,IAAI,eAAe,IAAI,IAAI,GAAG,KAAK;GAGnC,IAAI,IAAI,WAAW,QACjB,OAAO,KACL,4BAA4B,KAAK,YAAY,UAAU,IACrD,WAAW,2BAA2B,OACvC,KAAK,KAAK,UAAU,IAAI,GAC3B;EACJ;EAEA,OAAO;CACT;AACF"}
|
|
@@ -1,48 +1,69 @@
|
|
|
1
1
|
import { PlTreeEntry, PlTreeRootsEntry } from "./accessors.js";
|
|
2
2
|
import { ExtendedResourceData } from "./state.js";
|
|
3
3
|
import { PruningFunction, TraversalMode } from "./sync.js";
|
|
4
|
-
import {
|
|
4
|
+
import { PersistedTree } from "./persisted_tree.js";
|
|
5
|
+
import { Filter, FinalResourceDataPredicate, PlClient, ResourceSignature, ResourceType, SignedResourceId } from "@milaboratories/pl-client";
|
|
5
6
|
import { MiLogger } from "@milaboratories/ts-helpers";
|
|
6
|
-
|
|
7
7
|
//#region src/synchronized_tree.d.ts
|
|
8
8
|
type StatLoggingMode = "cumulative" | "per-request";
|
|
9
|
-
type SynchronizedTreeOps = {
|
|
10
|
-
/** Override final predicate from the PlClient */
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
export type SynchronizedTreeOps = {
|
|
10
|
+
/** Override final predicate from the PlClient */
|
|
11
|
+
finalPredicateOverride?: FinalResourceDataPredicate;
|
|
12
|
+
/** Pruning function for legacy fallback path. */
|
|
13
|
+
pruning?: PruningFunction;
|
|
14
|
+
/** ResourceTree field filter for modern backend path. */
|
|
15
|
+
fieldFilter?: Filter;
|
|
16
|
+
/** ResourceTree traversal stop rules for modern backend path. */
|
|
17
|
+
traverseStopRules?: Filter;
|
|
18
|
+
/** Interval after last sync to sleep before the next one */
|
|
19
|
+
pollingInterval: number;
|
|
20
|
+
/** For how long to continue polling after the last derived value access */
|
|
21
|
+
stopPollingDelay: number;
|
|
22
|
+
/** If one of the values, tree will log stats of each polling request */
|
|
23
|
+
logStat?: StatLoggingMode;
|
|
24
|
+
/** Timeout for initial tree loading. If not specified, will use default for RO tx from pl-client. */
|
|
25
|
+
initialTreeLoadingTimeout?: number;
|
|
26
|
+
/** Controls which tree-loading path to use. Default `"auto"`. */
|
|
18
27
|
traversalMode?: TraversalMode;
|
|
28
|
+
/** A previously persisted mirror to seed the tree with, before its first refresh, so that
|
|
29
|
+
* refresh transfers only what changed while the tree was gone.
|
|
30
|
+
*
|
|
31
|
+
* A snapshot that cannot be applied, or does not belong to this tree, is logged and dropped,
|
|
32
|
+
* leaving an ordinary cold open.
|
|
33
|
+
*
|
|
34
|
+
* A snapshot that applies but whose ids are dead is NOT handled here: its resources become
|
|
35
|
+
* this tree's seeds, so the first refresh fails and {@link init} rejects, where a cold open
|
|
36
|
+
* would have succeeded. Establishing that the signatures are still live is the caller's job
|
|
37
|
+
* (see {@link PersistedTree.witness}), as is deciding what to do when the first refresh is
|
|
38
|
+
* refused anyway. Ignored for trees with shared-type seeds, which rediscover their roots. */
|
|
39
|
+
restoreFrom?: PersistedTree;
|
|
19
40
|
};
|
|
20
41
|
/** An explicit resource to serve as a tree root. Several explicit seeds may be passed. */
|
|
21
|
-
type ExplicitRootSeed = {
|
|
42
|
+
export type ExplicitRootSeed = {
|
|
22
43
|
kind: "resource";
|
|
23
44
|
root: SignedResourceId;
|
|
24
45
|
};
|
|
25
46
|
/** Discovers, as roots, every resource of this type shared with the current user.
|
|
26
47
|
* Matched against SharedResource.resourceType by NAME (version optional/ignored). The
|
|
27
48
|
* discovered set is DYNAMIC — roots appear/disappear as grants are added/revoked/expire. */
|
|
28
|
-
type SharedTypeSeed = {
|
|
49
|
+
export type SharedTypeSeed = {
|
|
29
50
|
kind: "shared";
|
|
30
51
|
resourceType: ResourceType;
|
|
31
52
|
};
|
|
32
|
-
type TreeSeed = ExplicitRootSeed | SharedTypeSeed;
|
|
53
|
+
export type TreeSeed = ExplicitRootSeed | SharedTypeSeed;
|
|
33
54
|
/** The poll-cadence policy, as a pure function of the last cycle's outcome.
|
|
34
55
|
*
|
|
35
56
|
* `configuredMs` is the tree's static `pollingInterval` and acts as the lower bound, so no
|
|
36
57
|
* link can be polled faster than configured. `rttMs` raises that bound on a slow link.
|
|
37
58
|
* `currentMs` is the interval in force for the cycle that just finished, which is what the
|
|
38
59
|
* idle backoff compounds on. */
|
|
39
|
-
declare function derivePollingInterval(opts: {
|
|
60
|
+
export declare function derivePollingInterval(opts: {
|
|
40
61
|
configuredMs: number;
|
|
41
62
|
currentMs: number;
|
|
42
63
|
rttMs: number | undefined;
|
|
43
64
|
changed: boolean;
|
|
44
65
|
}): number;
|
|
45
|
-
declare class SynchronizedTreeState {
|
|
66
|
+
export declare class SynchronizedTreeState {
|
|
46
67
|
private readonly pl;
|
|
47
68
|
private readonly logger?;
|
|
48
69
|
private readonly finalPredicate;
|
|
@@ -61,9 +82,36 @@ declare class SynchronizedTreeState {
|
|
|
61
82
|
private readonly sharedSeeds;
|
|
62
83
|
/** Roots discovered for shared-type seeds on the last discovery poll. */
|
|
63
84
|
private discoveredRoots;
|
|
85
|
+
/** Bumped once per refresh cycle that brought something new: a resource appeared, changed,
|
|
86
|
+
* or became final. Lets a holder tell whether the tree has moved since it last persisted
|
|
87
|
+
* it, without diffing state. Read through {@link changeGeneration}. */
|
|
88
|
+
private changeGenerationCounter;
|
|
89
|
+
/** Whether a snapshot was actually applied. Read through
|
|
90
|
+
* {@link wasRestoredFromSnapshot}. */
|
|
91
|
+
private restoredFromSnapshot;
|
|
64
92
|
private constructor();
|
|
65
93
|
/** The current protected root set: explicit roots plus the latest discovered roots. */
|
|
66
94
|
private currentRootSet;
|
|
95
|
+
/** How many refresh cycles brought something new. Only ever increases. Equal values at two
|
|
96
|
+
* points in time mean nothing was added, changed or settled in between, which is what makes
|
|
97
|
+
* a periodic snapshot write skippable on an idle tree. */
|
|
98
|
+
get changeGeneration(): number;
|
|
99
|
+
/** True only if a snapshot was actually applied as this tree's initial state. A snapshot can
|
|
100
|
+
* be supplied and still be refused (wrong roots, or state the update call will not accept),
|
|
101
|
+
* in which case this stays false and the tree started empty like any other. Passing
|
|
102
|
+
* `restoreFrom` is therefore not evidence of a warm start; this is. */
|
|
103
|
+
get wasRestoredFromSnapshot(): boolean;
|
|
104
|
+
/** Captures the current mirror for persistence.
|
|
105
|
+
*
|
|
106
|
+
* Must be called before {@link terminate}: terminating invalidates the tree, and capturing
|
|
107
|
+
* an invalidated tree is refused rather than silently written. */
|
|
108
|
+
capture(witness: ResourceSignature): PersistedTree;
|
|
109
|
+
/** Installs a snapshot as this tree's state. Returns false if the snapshot was refused, in
|
|
110
|
+
* which case the tree is left as it was and the open proceeds cold.
|
|
111
|
+
*
|
|
112
|
+
* Only meaningful before the first refresh, which is why it is private and driven from
|
|
113
|
+
* {@link init}: replacing the state of a running tree would strand its observers. */
|
|
114
|
+
private restore;
|
|
67
115
|
/** Resolves the single root for the backward-compatible single-root accessors, throwing
|
|
68
116
|
* if the tree does not have exactly one root (guards legacy callers against multi-root). */
|
|
69
117
|
private soleRoot;
|
|
@@ -137,5 +185,4 @@ declare class SynchronizedTreeState {
|
|
|
137
185
|
static init(pl: PlClient, seeds: SignedResourceId | TreeSeed | TreeSeed[], ops: SynchronizedTreeOps, logger?: MiLogger): Promise<SynchronizedTreeState>;
|
|
138
186
|
}
|
|
139
187
|
//#endregion
|
|
140
|
-
export { ExplicitRootSeed, SharedTypeSeed, SynchronizedTreeOps, SynchronizedTreeState, TreeSeed, derivePollingInterval };
|
|
141
188
|
//# sourceMappingURL=synchronized_tree.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"synchronized_tree.d.ts","names":[],"sources":["../src/synchronized_tree.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"synchronized_tree.d.ts","names":[],"sources":["../src/synchronized_tree.ts"],"mappings":";;;;;;;KAmDK;YAEO;;EAEV,yBAAyB;;EAGzB,UAAU;;EAGV,cAAc;;EAGd,oBAAoB;;EAGpB;;EAEA;;EAGA,UAAU;;EAGV;;EAGA,gBAAgB;;;;;;;;;;;;EAahB,cAAc;;;YAIJ;EAAqB;EAAkB,MAAM;;;;;YAK7C;EAAmB;EAAgB,cAAc;;YAEjD,WAAW,mBAAmB;;;;;;;wBA+C1B,sBAAsB;EACpC;EACA;EACA;EACA;;qBA0BW;mBA6BQ;mBAGA;mBA/BF;UACT;mBACS;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;mBAGA;;mBAEA;;UAET;;;;UAKA;;;UAIA;UAED;;UAwCC;;;;MAOG;;;;;MAQA;;;;;EAQJ,QAAQ,SAAS,oBAAoB;;;;;;UAUpC;;;UA2BA;;EAUD,SAAS,MAAM,mBAAmB;;;;EAQlC,MAAM,MAAM,mBAAmB;;;EAU/B,cAAc;;;EAOR,gBAAgB;UAKrB;UACA;;;UAIA;;;;;;;;UASA;;UAUA;;UAoBA;;UAOA;;UAKA;;UAEA;;UAGM;UAmBA;;;;;;;UAmCA;;UAsBN;UAEM;;;;;EAoIP,aAAa;;;;;EAQP,aAAa;;EAYb,4BAA4B;;;;;;;;SAYrB,KAClB,IAAI,UACJ,OAAO,mBAAmB,WAAW,YACrC,KAAK,qBACL,SAAS,WAAQ,QAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PlTreeEntry, PlTreeRootsEntry } from "./accessors.js";
|
|
2
2
|
import { PlTreeState, TreeStateUpdateError } from "./state.js";
|
|
3
3
|
import { constructTreeLoadingRequest, initialTreeLoadingStat, loadTreeState } from "./sync.js";
|
|
4
|
+
import { captureTreeState, restoreTreeState } from "./persisted_tree.js";
|
|
4
5
|
import { isTimeoutOrCancelError, isUnauthenticated, isUnimplementedError } from "@milaboratories/pl-client";
|
|
5
6
|
import { PollingComputableHooks } from "@milaboratories/computable";
|
|
6
7
|
import * as tp from "node:timers/promises";
|
|
@@ -56,7 +57,7 @@ const DISCOVERY_INTERVAL_MS = 3e3;
|
|
|
56
57
|
* `resourcesUnchanged` is excluded by design, since a cycle that only re-fetched unchanged
|
|
57
58
|
* state is exactly the idle case the backoff exists for. */
|
|
58
59
|
function countedChanges(stat) {
|
|
59
|
-
return stat.resourcesNew + stat.resourcesChanged + stat.resourcesMarkedFinal;
|
|
60
|
+
return stat.resourcesNew + stat.resourcesChanged + stat.resourcesMarkedFinal + stat.fieldsRemoved;
|
|
60
61
|
}
|
|
61
62
|
/** The poll-cadence policy, as a pure function of the last cycle's outcome.
|
|
62
63
|
*
|
|
@@ -89,6 +90,13 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
89
90
|
sharedSeeds;
|
|
90
91
|
/** Roots discovered for shared-type seeds on the last discovery poll. */
|
|
91
92
|
discoveredRoots = [];
|
|
93
|
+
/** Bumped once per refresh cycle that brought something new: a resource appeared, changed,
|
|
94
|
+
* or became final. Lets a holder tell whether the tree has moved since it last persisted
|
|
95
|
+
* it, without diffing state. Read through {@link changeGeneration}. */
|
|
96
|
+
changeGenerationCounter = 0;
|
|
97
|
+
/** Whether a snapshot was actually applied. Read through
|
|
98
|
+
* {@link wasRestoredFromSnapshot}. */
|
|
99
|
+
restoredFromSnapshot = false;
|
|
92
100
|
constructor(pl, seeds, ops, logger) {
|
|
93
101
|
this.pl = pl;
|
|
94
102
|
this.logger = logger;
|
|
@@ -110,6 +118,52 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
110
118
|
currentRootSet() {
|
|
111
119
|
return /* @__PURE__ */ new Set([...this.explicitRoots, ...this.discoveredRoots]);
|
|
112
120
|
}
|
|
121
|
+
/** How many refresh cycles brought something new. Only ever increases. Equal values at two
|
|
122
|
+
* points in time mean nothing was added, changed or settled in between, which is what makes
|
|
123
|
+
* a periodic snapshot write skippable on an idle tree. */
|
|
124
|
+
get changeGeneration() {
|
|
125
|
+
return this.changeGenerationCounter;
|
|
126
|
+
}
|
|
127
|
+
/** True only if a snapshot was actually applied as this tree's initial state. A snapshot can
|
|
128
|
+
* be supplied and still be refused (wrong roots, or state the update call will not accept),
|
|
129
|
+
* in which case this stays false and the tree started empty like any other. Passing
|
|
130
|
+
* `restoreFrom` is therefore not evidence of a warm start; this is. */
|
|
131
|
+
get wasRestoredFromSnapshot() {
|
|
132
|
+
return this.restoredFromSnapshot;
|
|
133
|
+
}
|
|
134
|
+
/** Captures the current mirror for persistence.
|
|
135
|
+
*
|
|
136
|
+
* Must be called before {@link terminate}: terminating invalidates the tree, and capturing
|
|
137
|
+
* an invalidated tree is refused rather than silently written. */
|
|
138
|
+
capture(witness) {
|
|
139
|
+
if (this.terminated) throw new Error("tree synchronization is terminated");
|
|
140
|
+
return captureTreeState(this.state, witness);
|
|
141
|
+
}
|
|
142
|
+
/** Installs a snapshot as this tree's state. Returns false if the snapshot was refused, in
|
|
143
|
+
* which case the tree is left as it was and the open proceeds cold.
|
|
144
|
+
*
|
|
145
|
+
* Only meaningful before the first refresh, which is why it is private and driven from
|
|
146
|
+
* {@link init}: replacing the state of a running tree would strand its observers. */
|
|
147
|
+
restore(snapshot) {
|
|
148
|
+
if (this.sharedSeeds.length > 0) {
|
|
149
|
+
this.logger?.warn("ignoring tree snapshot: trees with shared-type seeds are not restored");
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const roots = this.currentRootSet();
|
|
153
|
+
const snapshotRoots = new Set(snapshot.roots);
|
|
154
|
+
if (snapshotRoots.size !== roots.size || ![...snapshotRoots].every((r) => roots.has(r))) {
|
|
155
|
+
this.logger?.warn("ignoring tree snapshot: its roots are not this tree's roots");
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const restored = restoreTreeState(snapshot, this.finalPredicate, {
|
|
159
|
+
roots,
|
|
160
|
+
logger: this.logger
|
|
161
|
+
});
|
|
162
|
+
if (restored === void 0) return false;
|
|
163
|
+
this.state = restored;
|
|
164
|
+
this.restoredFromSnapshot = true;
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
113
167
|
/** Resolves the single root for the backward-compatible single-root accessors, throwing
|
|
114
168
|
* if the tree does not have exactly one root (guards legacy callers against multi-root). */
|
|
115
169
|
soleRoot() {
|
|
@@ -268,7 +322,9 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
268
322
|
}
|
|
269
323
|
const changesBefore = countedChanges(stat);
|
|
270
324
|
await this.refresh(stat);
|
|
271
|
-
|
|
325
|
+
const changed = countedChanges(stat) > changesBefore;
|
|
326
|
+
if (changed) this.changeGenerationCounter++;
|
|
327
|
+
this.updatePollingInterval(changed);
|
|
272
328
|
if (this.logStat && this.logger) this.logger.info(`Tree stat (success, after ${Date.now() - lastUpdate}ms): ${JSON.stringify(stat)}`);
|
|
273
329
|
lastUpdate = Date.now();
|
|
274
330
|
if (toNotify !== void 0) for (const n of toNotify) n.resolve();
|
|
@@ -339,14 +395,16 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
339
395
|
*/
|
|
340
396
|
static async init(pl, seeds, ops, logger) {
|
|
341
397
|
const tree = new SynchronizedTreeState(pl, normalizeSeeds(seeds), ops, logger);
|
|
342
|
-
const
|
|
398
|
+
const restored = ops.restoreFrom !== void 0 && tree.restore(ops.restoreFrom);
|
|
399
|
+
const stat = initialTreeLoadingStat();
|
|
343
400
|
let ok = false;
|
|
344
401
|
try {
|
|
345
402
|
await tree.discover();
|
|
346
403
|
await tree.refresh(stat, { timeout: ops.initialTreeLoadingTimeout });
|
|
347
404
|
ok = true;
|
|
348
405
|
} finally {
|
|
349
|
-
if (stat
|
|
406
|
+
if (countedChanges(stat) > 0) tree.changeGenerationCounter++;
|
|
407
|
+
if (ops.logStat && logger) logger.info(`Tree stat (initial load, ${ok ? "success" : "failure"}, ${restored ? "restored from snapshot" : "cold"}): ${JSON.stringify(stat)}`);
|
|
350
408
|
}
|
|
351
409
|
return tree;
|
|
352
410
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"synchronized_tree.js","names":[],"sources":["../src/synchronized_tree.ts"],"sourcesContent":["import { PollingComputableHooks } from \"@milaboratories/computable\";\nimport { PlTreeEntry, PlTreeRootsEntry } from \"./accessors\";\nimport type {\n FinalResourceDataPredicate,\n PlClient,\n ResourceType,\n SignedResourceId,\n TxOps,\n} from \"@milaboratories/pl-client\";\nimport type { Filter } from \"@milaboratories/pl-client\";\nimport {\n isUnauthenticated,\n isTimeoutOrCancelError,\n isUnimplementedError,\n} from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData } from \"./state\";\nimport { PlTreeState, TreeStateUpdateError } from \"./state\";\nimport type { PruningFunction, TraversalMode, TreeLoadingStat } from \"./sync\";\nimport { constructTreeLoadingRequest, initialTreeLoadingStat, loadTreeState } from \"./sync\";\nimport * as tp from \"node:timers/promises\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\n\n/** Hard floor between consecutive tree-refresh calls.\n * Applies even when {@link scheduleOnNextState} has woken the loop early,\n * preventing tight polling loops during rapid state transitions. */\nconst MIN_POLLING_INTERVAL_MS = 100;\n\n/** Ceiling for the adaptive poll interval. Caps how stale an idle tree can get before the\n * next look, and bounds how far the idle backoff can push the interval out. */\nconst MAX_POLLING_INTERVAL_MS = 5_000;\n\n/** Applied to the interval after a cycle that changed nothing. An idle tree walks its\n * interval out towards {@link MAX_POLLING_INTERVAL_MS} instead of re-polling at full rate;\n * the first cycle that changes anything resets it. */\nconst IDLE_BACKOFF_MULTIPLIER = 1.5;\n\n/** The client's measured RTT becomes an interval floor, scaled by this. Every refresh costs\n * at least one round trip, so polling faster than a small multiple of the RTT only queues\n * round-trips the link cannot service. This is what replaces the fixed interval on a\n * high-latency link; on a fast link the configured `pollingInterval` still dominates. */\nconst RTT_POLL_FACTOR = 2;\n\n/** Ceiling for the RTT-derived floor. The estimate is sampled at connect time and never\n * re-sampled, so without a bound one slow ping pins the cadence high for the whole session.\n * Mirrors `MAX_ADAPTIVE_REQUEST_TIMEOUT` on the deadline side: past this the link is stuck\n * rather than slow, and spacing polls further only delays noticing it recovered. */\nconst MAX_RTT_POLL_INTERVAL_MS = 30_000;\n\ntype StatLoggingMode = \"cumulative\" | \"per-request\";\n\nexport type SynchronizedTreeOps = {\n /** Override final predicate from the PlClient */\n finalPredicateOverride?: FinalResourceDataPredicate;\n\n /** Pruning function for legacy fallback path. */\n pruning?: PruningFunction;\n\n /** ResourceTree field filter for modern backend path. */\n fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules for modern backend path. */\n traverseStopRules?: Filter;\n\n /** Interval after last sync to sleep before the next one */\n pollingInterval: number;\n /** For how long to continue polling after the last derived value access */\n stopPollingDelay: number;\n\n /** If one of the values, tree will log stats of each polling request */\n logStat?: StatLoggingMode;\n\n /** Timeout for initial tree loading. If not specified, will use default for RO tx from pl-client. */\n initialTreeLoadingTimeout?: number;\n\n /** Controls which tree-loading path to use. Default `\"auto\"`. */\n traversalMode?: TraversalMode;\n};\n\n/** An explicit resource to serve as a tree root. Several explicit seeds may be passed. */\nexport type ExplicitRootSeed = { kind: \"resource\"; root: SignedResourceId };\n\n/** Discovers, as roots, every resource of this type shared with the current user.\n * Matched against SharedResource.resourceType by NAME (version optional/ignored). The\n * discovered set is DYNAMIC — roots appear/disappear as grants are added/revoked/expire. */\nexport type SharedTypeSeed = { kind: \"shared\"; resourceType: ResourceType };\n\nexport type TreeSeed = ExplicitRootSeed | SharedTypeSeed;\n\n/** Normalizes the {@link SynchronizedTreeState.init} seed argument — a bare\n * {@link SignedResourceId}, a single {@link TreeSeed}, or an array — to `TreeSeed[]`, so\n * every existing single-root caller is unchanged. */\nfunction normalizeSeeds(seeds: SignedResourceId | TreeSeed | TreeSeed[]): TreeSeed[] {\n if (Array.isArray(seeds)) return seeds;\n if (typeof seeds === \"object\" && seeds !== null && \"kind\" in seeds) return [seeds];\n // bare SignedResourceId\n return [{ kind: \"resource\", root: seeds }];\n}\n\n/** How often a tree with shared-type seeds re-polls ListUserResources to reconcile its\n * discovered roots.\n *\n * Discovery (a full ListUserResources stream) is far heavier than an ordinary incremental\n * refresh, so it must NOT run on every refresh tick. This is a wall-clock interval rather\n * than a count of iterations: the refresh cadence is adaptive (it stretches towards\n * {@link MAX_POLLING_INTERVAL_MS} on an idle tree and scales with RTT on a slow link),\n * so a fixed iteration count would let discovery latency drift out with it. Keeping it in\n * milliseconds pins the latency of noticing a new/removed share regardless of cadence, which\n * is what a human-driven share flow cares about.\n *\n * Only trees with shared-type seeds gate on this; {@link discover} is a no-op for single-root\n * and explicit-seed trees (empty `sharedSeeds`), so the value never affects them. */\nconst DISCOVERY_INTERVAL_MS = 3_000;\n\n/** Counters that mean \"this cycle brought something new\". Deliberately not the full change\n * breakdown: those fields are sub-counts of `resourcesChanged` and would double-count.\n * `resourcesUnchanged` is excluded by design, since a cycle that only re-fetched unchanged\n * state is exactly the idle case the backoff exists for. */\nfunction countedChanges(stat: TreeLoadingStat): number {\n return stat.resourcesNew + stat.resourcesChanged + stat.resourcesMarkedFinal;\n}\n\n/** The poll-cadence policy, as a pure function of the last cycle's outcome.\n *\n * `configuredMs` is the tree's static `pollingInterval` and acts as the lower bound, so no\n * link can be polled faster than configured. `rttMs` raises that bound on a slow link.\n * `currentMs` is the interval in force for the cycle that just finished, which is what the\n * idle backoff compounds on. */\nexport function derivePollingInterval(opts: {\n configuredMs: number;\n currentMs: number;\n rttMs: number | undefined;\n changed: boolean;\n}): number {\n const { configuredMs, currentMs, rttMs, changed } = opts;\n\n // The cap applies to the RTT-derived part only, so `configuredMs` stays an absolute lower\n // bound even if it is ever set above the cap.\n const floor =\n rttMs === undefined\n ? configuredMs\n : Math.max(\n configuredMs,\n Math.min(MAX_RTT_POLL_INTERVAL_MS, Math.ceil(rttMs * RTT_POLL_FACTOR)),\n );\n\n const next = changed ? floor : Math.max(floor, currentMs * IDLE_BACKOFF_MULTIPLIER);\n\n // The ceiling never cuts below the floor: a link slower than MAX_POLLING_INTERVAL_MS still\n // gets its RTT-derived spacing rather than being forced to re-poll early.\n return Math.max(floor, Math.min(MAX_POLLING_INTERVAL_MS, next));\n}\n\ntype ScheduledRefresh = {\n resolve: () => void;\n reject: (err: any) => void;\n};\n\nexport class SynchronizedTreeState {\n private readonly finalPredicate: FinalResourceDataPredicate;\n private state: PlTreeState;\n private readonly pollingInterval: number;\n private readonly pruning?: PruningFunction;\n private readonly fieldFilter?: Filter;\n private readonly traverseStopRules?: Filter;\n private readonly traversalMode: TraversalMode;\n private readonly logStat?: StatLoggingMode;\n private readonly hooks: PollingComputableHooks;\n private readonly abortController = new AbortController();\n\n /** Explicit-resource seeds: fixed roots, present from construction. */\n private readonly explicitRoots: SignedResourceId[];\n /** Shared-type seeds (discovered roots), if any. */\n private readonly sharedSeeds: SharedTypeSeed[];\n /** Roots discovered for shared-type seeds on the last discovery poll. */\n private discoveredRoots: SignedResourceId[] = [];\n\n private constructor(\n private readonly pl: PlClient,\n seeds: TreeSeed[],\n ops: SynchronizedTreeOps,\n private readonly logger?: MiLogger,\n ) {\n const {\n finalPredicateOverride,\n pruning,\n fieldFilter,\n traverseStopRules,\n traversalMode,\n pollingInterval,\n stopPollingDelay,\n logStat,\n } = ops;\n this.pruning = pruning;\n this.fieldFilter = fieldFilter;\n this.traverseStopRules = traverseStopRules;\n this.traversalMode = traversalMode ?? \"auto\";\n this.pollingInterval = pollingInterval;\n this.effectivePollingInterval = pollingInterval;\n this.finalPredicate = finalPredicateOverride ?? pl.finalPredicate;\n this.logStat = logStat;\n\n this.explicitRoots = seeds\n .filter((s): s is ExplicitRootSeed => s.kind === \"resource\")\n .map((s) => s.root);\n this.sharedSeeds = seeds.filter((s): s is SharedTypeSeed => s.kind === \"shared\");\n\n this.state = new PlTreeState(this.currentRootSet(), this.finalPredicate);\n this.hooks = new PollingComputableHooks(\n () => this.startUpdating(),\n () => this.stopUpdating(),\n { stopDebounce: stopPollingDelay },\n (resolve, reject) => this.scheduleOnNextState(resolve, reject),\n );\n }\n\n /** The current protected root set: explicit roots plus the latest discovered roots. */\n private currentRootSet(): Set<SignedResourceId> {\n return new Set([...this.explicitRoots, ...this.discoveredRoots]);\n }\n\n /** Resolves the single root for the backward-compatible single-root accessors, throwing\n * if the tree does not have exactly one root (guards legacy callers against multi-root). */\n private soleRoot(): SignedResourceId {\n const roots = this.currentRootSet();\n if (roots.size !== 1)\n throw new Error(\n `single-root accessor used on a tree with ${roots.size} roots; use rootsEntry() instead`,\n );\n return roots.values().next().value!;\n }\n\n /** @deprecated use \"entry\" instead */\n public accessor(rid?: SignedResourceId): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return this.entry(rid);\n }\n\n /** Backward-compatible single-root entry. With no `rid` it returns the sole root's entry\n * and THROWS if the tree has zero or more than one root. An explicit `rid` addresses any\n * resource in the heap, as today. */\n public entry(rid?: SignedResourceId): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeEntry(\n { treeProvider: () => this.state, hooks: this.hooks },\n rid ?? this.soleRoot(),\n );\n }\n\n /** Reactive provider for the current root SET. Reading it inside a Computable tracks the\n * set as a dependency, so the Computable recomputes when discovered roots appear/disappear. */\n public rootsEntry(): PlTreeRootsEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeRootsEntry({ treeProvider: () => this.state, hooks: this.hooks });\n }\n\n /** Can be used to externally kick off the synchronization polling loop, and\n * await for the first synchronization to happen. */\n public async refreshState(): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n await this.hooks.refreshState();\n }\n\n private currentLoopDelayInterrupt: AbortController | undefined = undefined;\n private scheduledOnNextState: ScheduledRefresh[] = [];\n\n /** Interval actually used for the current wait. Starts at the configured `pollingInterval`\n * and is re-derived after every cycle by {@link updatePollingInterval}. */\n private effectivePollingInterval: number;\n\n /** Re-derives {@link effectivePollingInterval} after a cycle.\n *\n * Two independent effects. The floor scales with the client's measured RTT, so a\n * high-latency link stops queueing round-trips it cannot service. On top of that, a cycle\n * that changed nothing multiplies the interval out towards\n * {@link MAX_POLLING_INTERVAL_MS}, while any change snaps it straight back to the floor so\n * an active tree stays responsive. */\n private updatePollingInterval(changed: boolean): void {\n this.effectivePollingInterval = derivePollingInterval({\n configuredMs: this.pollingInterval,\n currentMs: this.effectivePollingInterval,\n rttMs: this.pl.rttEstimateMs,\n changed,\n });\n }\n\n /** Called from computable hooks when external observer asks for state refresh */\n private scheduleOnNextState(resolve: () => void, reject: (err: any) => void): void {\n if (this.terminated) reject(new Error(\"tree synchronization is terminated\"));\n else {\n this.scheduledOnNextState.push({ resolve, reject });\n // Someone is waiting on fresh state, so this tree is not idle after all: drop any\n // accumulated backoff, otherwise the cycles right after a nudge stay slow. Routed\n // through the policy rather than assigning the configured value directly, so the RTT\n // floor survives the reset. Assigning it raw would poll a high-latency link faster than\n // it can answer, and the interval would stay there until the next cycle that completes:\n // the error path never reaches updatePollingInterval, so a failing nudged refresh would\n // keep retrying at the un-floored rate.\n this.updatePollingInterval(true);\n if (this.currentLoopDelayInterrupt) {\n this.currentLoopDelayInterrupt.abort();\n this.currentLoopDelayInterrupt = undefined;\n }\n }\n }\n\n /** Called from observer */\n private startUpdating(): void {\n if (this.terminated) return;\n this.keepRunning = true;\n if (this.currentLoop === undefined) this.currentLoop = this.mainLoop();\n }\n\n /** Called from observer */\n private stopUpdating(): void {\n this.keepRunning = false;\n }\n\n /** If true, main loop will continue polling pl state. */\n private keepRunning = false;\n /** Actual state of main loop. */\n private currentLoop: Promise<void> | undefined = undefined;\n\n /** Executed from the main loop, and initialization procedure. */\n private async refresh(stats?: TreeLoadingStat, txOps?: TxOps): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n try {\n await this.loadAndApply(stats, txOps);\n } catch (e) {\n // Discovery-tree self-heal: a discovered root whose grant was revoked/expired fails the whole\n // ResourceTree poll with Unauthenticated. Re-discover (drops dead roots) and retry once. This is\n // self-discriminating — a genuinely dead session also fails discover()'s own call, so real auth\n // loss still propagates. Only for discovery trees; explicit-root trees propagate as-is.\n if (this.sharedSeeds.length > 0 && isUnauthenticated(e)) {\n this.logger?.warn(\n \"discovery tree: Unauthenticated on ResourceTree (likely revoked/expired root); re-discovering and retrying\",\n );\n await this.discover();\n await this.loadAndApply(stats, txOps);\n } else throw e;\n }\n }\n\n private async loadAndApply(stats?: TreeLoadingStat, txOps?: TxOps): Promise<void> {\n const request = constructTreeLoadingRequest(this.state, {\n pruningFunction: this.pruning,\n fieldFilter: this.fieldFilter,\n traverseStopRules: this.traverseStopRules,\n });\n // A shared-type-seed tree with no currently-discovered roots is legitimately empty:\n // there is nothing to traverse, and tx.resourceTree([]) would throw \"at least one seed\n // must be provided\". Skip the backend load and leave the (empty) state as-is — discovery\n // adds roots later via setRoots(), which schedules the next refresh. Explicit-root trees\n // never hit this (their root set is non-empty by construction).\n if (request.seedResources.length === 0 && request.finalResources.size === 0) return;\n const data = await this.pl.withReadTx(\n \"ReadingTree\",\n async (tx) => {\n return await loadTreeState(\n tx,\n request,\n stats,\n this.pl.serverInfo.capabilities ?? [],\n this.traversalMode,\n this.logger,\n );\n },\n txOps,\n );\n this.state.updateFromResourceData(data, { allowOrphanInputs: true, stat: stats });\n }\n\n /** Discovery sync for shared-type seeds: re-polls `ListUserResources` (gRPC-only) and\n * reconciles the discovered root set against the heap. A longer poll result adds roots; a\n * shorter one removes them (grant revoked/expired) — the removed roots' subtrees cascade\n * to collection via the ordinary refcount GC ({@link PlTreeState.setRoots}). No-op when the\n * tree has no shared-type seeds, or silently no-op on a REST client where `ListUserResources`\n * is unavailable. */\n private async discover(): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n if (this.sharedSeeds.length === 0) return;\n\n const discovered = new Set<SignedResourceId>();\n for (const seed of this.sharedSeeds) {\n let ids: SignedResourceId[];\n try {\n // match by name only (permissive; ignores version, so it survives schema bumps)\n ids = await this.pl.userResources.listSharedResourcesByType(seed.resourceType.name);\n } catch (e: unknown) {\n if (isUnimplementedError(e)) continue;\n throw e;\n }\n for (const id of ids) discovered.add(id);\n }\n\n this.discoveredRoots = [...discovered];\n this.state.setRoots(this.currentRootSet());\n }\n\n /** If true this tree state is permanently terminaed. */\n private terminated = false;\n\n private async mainLoop() {\n // Always collected, even when not logging: the change counters drive the idle backoff\n // below. Counter bumps are cheap next to the round trip they describe.\n let stat = initialTreeLoadingStat();\n\n let lastUpdate = Date.now();\n\n // paces the discovery poll for shared-type seeds; 0 forces discovery on the first pass.\n let lastDiscovery = 0;\n\n while (true) {\n if (!this.keepRunning || this.terminated) break;\n\n // saving those who want to be notified about new state here\n // because those who will be added during the tree retrieval\n // should be notified only on the next round\n let toNotify: ScheduledRefresh[] | undefined = undefined;\n if (this.scheduledOnNextState.length > 0) {\n toNotify = this.scheduledOnNextState;\n this.scheduledOnNextState = [];\n }\n\n try {\n // resetting stats if we were asked to collect non-cumulative stats\n if (this.logStat === \"per-request\") stat = initialTreeLoadingStat();\n\n // discovery sync for shared-type seeds: reconcile the discovered root set before\n // refreshing, so newly discovered roots are materialized in this same iteration.\n if (this.sharedSeeds.length > 0 && Date.now() - lastDiscovery >= DISCOVERY_INTERVAL_MS) {\n await this.discover();\n lastDiscovery = Date.now();\n }\n\n // Change counters before the refresh, so the delta tells us whether this single cycle\n // brought anything new. Works in both stat modes: \"per-request\" resets to 0 above.\n const changesBefore = countedChanges(stat);\n\n // actual tree synchronization\n await this.refresh(stat);\n\n this.updatePollingInterval(countedChanges(stat) > changesBefore);\n\n // logging stats if we were asked to\n if (this.logStat && this.logger)\n this.logger.info(\n `Tree stat (success, after ${Date.now() - lastUpdate}ms): ${JSON.stringify(stat)}`,\n );\n lastUpdate = Date.now();\n\n // notifying that we got new state\n if (toNotify !== undefined) for (const n of toNotify) n.resolve();\n } catch (e: any) {\n // logging stats if we were asked to (even if error occured)\n if (this.logStat && this.logger)\n this.logger.info(\n `Tree stat (error, after ${Date.now() - lastUpdate}ms): ${JSON.stringify(stat)}`,\n );\n lastUpdate = Date.now();\n\n // notifying that we failed to refresh the state\n if (toNotify !== undefined) for (const n of toNotify) n.reject(e);\n\n // catching tree update errors, as they may leave our tree in inconsistent state\n if (e instanceof TreeStateUpdateError) {\n // important error logging, this should never happen\n this.logger?.error(e);\n\n // marking everybody who used previous state as changed\n this.state.invalidateTree(\"stat update error\");\n // creating new tree with the full current root set (re-discovered on next iteration)\n this.state = new PlTreeState(this.currentRootSet(), this.finalPredicate);\n\n // scheduling state update without delay\n continue;\n\n // unfortunately external observer may still see tree in its default\n // empty state, though this is best we can do in this exceptional\n // situation, and hope on caching layers inside computables to present\n // some stale state until we reconstruct the tree again\n } else this.logger?.warn(e);\n }\n\n if (!this.keepRunning || this.terminated) break;\n\n // Phase 1: mandatory floor — always wait at least MIN_POLLING_INTERVAL_MS.\n // Not interruptible by scheduleOnNextState; only termination aborts it.\n try {\n await tp.setTimeout(MIN_POLLING_INTERVAL_MS, undefined, {\n signal: this.abortController.signal,\n });\n } catch (e: unknown) {\n if (!isTimeoutOrCancelError(e)) throw new Error(\"Unexpected error\", { cause: e });\n if (this.abortController.signal.aborted) break;\n }\n\n if (!this.keepRunning || this.terminated) break;\n\n // Phase 2: optional remainder up to pollingInterval — interruptible by\n // scheduleOnNextState so that an external nudge wakes the loop promptly.\n if (this.scheduledOnNextState.length === 0) {\n const remaining = Math.max(0, this.effectivePollingInterval - MIN_POLLING_INTERVAL_MS);\n if (remaining > 0) {\n try {\n this.currentLoopDelayInterrupt = new AbortController();\n await tp.setTimeout(remaining, undefined, {\n signal: AbortSignal.any([\n this.abortController.signal,\n this.currentLoopDelayInterrupt.signal,\n ]),\n });\n } catch (e: unknown) {\n if (!isTimeoutOrCancelError(e)) throw new Error(\"Unexpected error\", { cause: e });\n if (this.abortController.signal.aborted) break;\n // Otherwise it was just the loop delay interrupt (scheduleOnNextState),\n // continue to the next iteration\n } finally {\n this.currentLoopDelayInterrupt = undefined;\n }\n }\n }\n }\n\n // reset only as a very last line\n this.currentLoop = undefined;\n }\n\n /**\n * Dumps the current state of the tree.\n * @returns An array of ExtendedResourceData objects representing the current state of the tree.\n */\n public dumpState(): ExtendedResourceData[] {\n return this.state.dumpState();\n }\n\n /**\n * Terminates the internal loop, and permanently destoys all internal state, so\n * all computables using this state will resolve to errors.\n * */\n public async terminate(): Promise<void> {\n this.keepRunning = false;\n this.terminated = true;\n this.abortController.abort();\n\n if (this.currentLoop === undefined) return;\n await this.currentLoop;\n\n this.state.invalidateTree(\"synchronization terminated for the tree\");\n }\n\n /** @deprecated */\n public async awaitSyncLoopTermination(): Promise<void> {\n if (this.currentLoop === undefined) return;\n await this.currentLoop;\n }\n\n /**\n * Initializes a synchronized tree from one or more seeds.\n *\n * @param seeds a bare {@link SignedResourceId} (the original single-root contract), a\n * single {@link TreeSeed}, or an array of seeds. Bare ids and explicit-resource seeds\n * become roots immediately; shared-type seeds discover their roots via `ListUserResources`.\n */\n public static async init(\n pl: PlClient,\n seeds: SignedResourceId | TreeSeed | TreeSeed[],\n ops: SynchronizedTreeOps,\n logger?: MiLogger,\n ) {\n const tree = new SynchronizedTreeState(pl, normalizeSeeds(seeds), ops, logger);\n\n const stat = ops.logStat ? initialTreeLoadingStat() : undefined;\n\n let ok = false;\n\n try {\n // resolve shared-type seeds before the first refresh so discovered roots load now\n await tree.discover();\n await tree.refresh(stat, {\n timeout: ops.initialTreeLoadingTimeout,\n });\n ok = true;\n } finally {\n // logging stats if we were asked to (even if error occured)\n if (stat && logger)\n logger.info(\n `Tree stat (initial load, ${ok ? \"success\" : \"failure\"}): ${JSON.stringify(stat)}`,\n );\n }\n\n return tree;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"synchronized_tree.js","names":[],"sources":["../src/synchronized_tree.ts"],"sourcesContent":["import { PollingComputableHooks } from \"@milaboratories/computable\";\nimport { PlTreeEntry, PlTreeRootsEntry } from \"./accessors\";\nimport type {\n FinalResourceDataPredicate,\n PlClient,\n ResourceSignature,\n ResourceType,\n SignedResourceId,\n TxOps,\n} from \"@milaboratories/pl-client\";\nimport type { Filter } from \"@milaboratories/pl-client\";\nimport {\n isUnauthenticated,\n isTimeoutOrCancelError,\n isUnimplementedError,\n} from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData } from \"./state\";\nimport { PlTreeState, TreeStateUpdateError } from \"./state\";\nimport type { PruningFunction, TraversalMode, TreeLoadingStat } from \"./sync\";\nimport { constructTreeLoadingRequest, initialTreeLoadingStat, loadTreeState } from \"./sync\";\nimport type { PersistedTree } from \"./persisted_tree\";\nimport { captureTreeState, restoreTreeState } from \"./persisted_tree\";\nimport * as tp from \"node:timers/promises\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\n\n/** Hard floor between consecutive tree-refresh calls.\n * Applies even when {@link scheduleOnNextState} has woken the loop early,\n * preventing tight polling loops during rapid state transitions. */\nconst MIN_POLLING_INTERVAL_MS = 100;\n\n/** Ceiling for the adaptive poll interval. Caps how stale an idle tree can get before the\n * next look, and bounds how far the idle backoff can push the interval out. */\nconst MAX_POLLING_INTERVAL_MS = 5_000;\n\n/** Applied to the interval after a cycle that changed nothing. An idle tree walks its\n * interval out towards {@link MAX_POLLING_INTERVAL_MS} instead of re-polling at full rate;\n * the first cycle that changes anything resets it. */\nconst IDLE_BACKOFF_MULTIPLIER = 1.5;\n\n/** The client's measured RTT becomes an interval floor, scaled by this. Every refresh costs\n * at least one round trip, so polling faster than a small multiple of the RTT only queues\n * round-trips the link cannot service. This is what replaces the fixed interval on a\n * high-latency link; on a fast link the configured `pollingInterval` still dominates. */\nconst RTT_POLL_FACTOR = 2;\n\n/** Ceiling for the RTT-derived floor. The estimate is sampled at connect time and never\n * re-sampled, so without a bound one slow ping pins the cadence high for the whole session.\n * Mirrors `MAX_ADAPTIVE_REQUEST_TIMEOUT` on the deadline side: past this the link is stuck\n * rather than slow, and spacing polls further only delays noticing it recovered. */\nconst MAX_RTT_POLL_INTERVAL_MS = 30_000;\n\ntype StatLoggingMode = \"cumulative\" | \"per-request\";\n\nexport type SynchronizedTreeOps = {\n /** Override final predicate from the PlClient */\n finalPredicateOverride?: FinalResourceDataPredicate;\n\n /** Pruning function for legacy fallback path. */\n pruning?: PruningFunction;\n\n /** ResourceTree field filter for modern backend path. */\n fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules for modern backend path. */\n traverseStopRules?: Filter;\n\n /** Interval after last sync to sleep before the next one */\n pollingInterval: number;\n /** For how long to continue polling after the last derived value access */\n stopPollingDelay: number;\n\n /** If one of the values, tree will log stats of each polling request */\n logStat?: StatLoggingMode;\n\n /** Timeout for initial tree loading. If not specified, will use default for RO tx from pl-client. */\n initialTreeLoadingTimeout?: number;\n\n /** Controls which tree-loading path to use. Default `\"auto\"`. */\n traversalMode?: TraversalMode;\n\n /** A previously persisted mirror to seed the tree with, before its first refresh, so that\n * refresh transfers only what changed while the tree was gone.\n *\n * A snapshot that cannot be applied, or does not belong to this tree, is logged and dropped,\n * leaving an ordinary cold open.\n *\n * A snapshot that applies but whose ids are dead is NOT handled here: its resources become\n * this tree's seeds, so the first refresh fails and {@link init} rejects, where a cold open\n * would have succeeded. Establishing that the signatures are still live is the caller's job\n * (see {@link PersistedTree.witness}), as is deciding what to do when the first refresh is\n * refused anyway. Ignored for trees with shared-type seeds, which rediscover their roots. */\n restoreFrom?: PersistedTree;\n};\n\n/** An explicit resource to serve as a tree root. Several explicit seeds may be passed. */\nexport type ExplicitRootSeed = { kind: \"resource\"; root: SignedResourceId };\n\n/** Discovers, as roots, every resource of this type shared with the current user.\n * Matched against SharedResource.resourceType by NAME (version optional/ignored). The\n * discovered set is DYNAMIC — roots appear/disappear as grants are added/revoked/expire. */\nexport type SharedTypeSeed = { kind: \"shared\"; resourceType: ResourceType };\n\nexport type TreeSeed = ExplicitRootSeed | SharedTypeSeed;\n\n/** Normalizes the {@link SynchronizedTreeState.init} seed argument — a bare\n * {@link SignedResourceId}, a single {@link TreeSeed}, or an array — to `TreeSeed[]`, so\n * every existing single-root caller is unchanged. */\nfunction normalizeSeeds(seeds: SignedResourceId | TreeSeed | TreeSeed[]): TreeSeed[] {\n if (Array.isArray(seeds)) return seeds;\n if (typeof seeds === \"object\" && seeds !== null && \"kind\" in seeds) return [seeds];\n // bare SignedResourceId\n return [{ kind: \"resource\", root: seeds }];\n}\n\n/** How often a tree with shared-type seeds re-polls ListUserResources to reconcile its\n * discovered roots.\n *\n * Discovery (a full ListUserResources stream) is far heavier than an ordinary incremental\n * refresh, so it must NOT run on every refresh tick. This is a wall-clock interval rather\n * than a count of iterations: the refresh cadence is adaptive (it stretches towards\n * {@link MAX_POLLING_INTERVAL_MS} on an idle tree and scales with RTT on a slow link),\n * so a fixed iteration count would let discovery latency drift out with it. Keeping it in\n * milliseconds pins the latency of noticing a new/removed share regardless of cadence, which\n * is what a human-driven share flow cares about.\n *\n * Only trees with shared-type seeds gate on this; {@link discover} is a no-op for single-root\n * and explicit-seed trees (empty `sharedSeeds`), so the value never affects them. */\nconst DISCOVERY_INTERVAL_MS = 3_000;\n\n/** Counters that mean \"this cycle brought something new\". Deliberately not the full change\n * breakdown: those fields are sub-counts of `resourcesChanged` and would double-count.\n * `resourcesUnchanged` is excluded by design, since a cycle that only re-fetched unchanged\n * state is exactly the idle case the backoff exists for. */\nfunction countedChanges(stat: TreeLoadingStat): number {\n // `fieldsRemoved` is included despite being a per-field count, because it is the one change\n // that never shows up in `resourcesChanged`: the removed-dynamic-field branch in\n // `updateFromResourceData` does not set its `changed` flag, so a cycle that only dropped a\n // field (and garbage-collected whatever it pointed at) otherwise reads as an idle cycle.\n // That double-counts a resource that both changed and lost a field, which is harmless here:\n // every caller compares this against an earlier value rather than reading it as a total.\n return stat.resourcesNew + stat.resourcesChanged + stat.resourcesMarkedFinal + stat.fieldsRemoved;\n}\n\n/** The poll-cadence policy, as a pure function of the last cycle's outcome.\n *\n * `configuredMs` is the tree's static `pollingInterval` and acts as the lower bound, so no\n * link can be polled faster than configured. `rttMs` raises that bound on a slow link.\n * `currentMs` is the interval in force for the cycle that just finished, which is what the\n * idle backoff compounds on. */\nexport function derivePollingInterval(opts: {\n configuredMs: number;\n currentMs: number;\n rttMs: number | undefined;\n changed: boolean;\n}): number {\n const { configuredMs, currentMs, rttMs, changed } = opts;\n\n // The cap applies to the RTT-derived part only, so `configuredMs` stays an absolute lower\n // bound even if it is ever set above the cap.\n const floor =\n rttMs === undefined\n ? configuredMs\n : Math.max(\n configuredMs,\n Math.min(MAX_RTT_POLL_INTERVAL_MS, Math.ceil(rttMs * RTT_POLL_FACTOR)),\n );\n\n const next = changed ? floor : Math.max(floor, currentMs * IDLE_BACKOFF_MULTIPLIER);\n\n // The ceiling never cuts below the floor: a link slower than MAX_POLLING_INTERVAL_MS still\n // gets its RTT-derived spacing rather than being forced to re-poll early.\n return Math.max(floor, Math.min(MAX_POLLING_INTERVAL_MS, next));\n}\n\ntype ScheduledRefresh = {\n resolve: () => void;\n reject: (err: any) => void;\n};\n\nexport class SynchronizedTreeState {\n private readonly finalPredicate: FinalResourceDataPredicate;\n private state: PlTreeState;\n private readonly pollingInterval: number;\n private readonly pruning?: PruningFunction;\n private readonly fieldFilter?: Filter;\n private readonly traverseStopRules?: Filter;\n private readonly traversalMode: TraversalMode;\n private readonly logStat?: StatLoggingMode;\n private readonly hooks: PollingComputableHooks;\n private readonly abortController = new AbortController();\n\n /** Explicit-resource seeds: fixed roots, present from construction. */\n private readonly explicitRoots: SignedResourceId[];\n /** Shared-type seeds (discovered roots), if any. */\n private readonly sharedSeeds: SharedTypeSeed[];\n /** Roots discovered for shared-type seeds on the last discovery poll. */\n private discoveredRoots: SignedResourceId[] = [];\n\n /** Bumped once per refresh cycle that brought something new: a resource appeared, changed,\n * or became final. Lets a holder tell whether the tree has moved since it last persisted\n * it, without diffing state. Read through {@link changeGeneration}. */\n private changeGenerationCounter = 0;\n\n /** Whether a snapshot was actually applied. Read through\n * {@link wasRestoredFromSnapshot}. */\n private restoredFromSnapshot = false;\n\n private constructor(\n private readonly pl: PlClient,\n seeds: TreeSeed[],\n ops: SynchronizedTreeOps,\n private readonly logger?: MiLogger,\n ) {\n const {\n finalPredicateOverride,\n pruning,\n fieldFilter,\n traverseStopRules,\n traversalMode,\n pollingInterval,\n stopPollingDelay,\n logStat,\n } = ops;\n this.pruning = pruning;\n this.fieldFilter = fieldFilter;\n this.traverseStopRules = traverseStopRules;\n this.traversalMode = traversalMode ?? \"auto\";\n this.pollingInterval = pollingInterval;\n this.effectivePollingInterval = pollingInterval;\n this.finalPredicate = finalPredicateOverride ?? pl.finalPredicate;\n this.logStat = logStat;\n\n this.explicitRoots = seeds\n .filter((s): s is ExplicitRootSeed => s.kind === \"resource\")\n .map((s) => s.root);\n this.sharedSeeds = seeds.filter((s): s is SharedTypeSeed => s.kind === \"shared\");\n\n this.state = new PlTreeState(this.currentRootSet(), this.finalPredicate);\n this.hooks = new PollingComputableHooks(\n () => this.startUpdating(),\n () => this.stopUpdating(),\n { stopDebounce: stopPollingDelay },\n (resolve, reject) => this.scheduleOnNextState(resolve, reject),\n );\n }\n\n /** The current protected root set: explicit roots plus the latest discovered roots. */\n private currentRootSet(): Set<SignedResourceId> {\n return new Set([...this.explicitRoots, ...this.discoveredRoots]);\n }\n\n /** How many refresh cycles brought something new. Only ever increases. Equal values at two\n * points in time mean nothing was added, changed or settled in between, which is what makes\n * a periodic snapshot write skippable on an idle tree. */\n public get changeGeneration(): number {\n return this.changeGenerationCounter;\n }\n\n /** True only if a snapshot was actually applied as this tree's initial state. A snapshot can\n * be supplied and still be refused (wrong roots, or state the update call will not accept),\n * in which case this stays false and the tree started empty like any other. Passing\n * `restoreFrom` is therefore not evidence of a warm start; this is. */\n public get wasRestoredFromSnapshot(): boolean {\n return this.restoredFromSnapshot;\n }\n\n /** Captures the current mirror for persistence.\n *\n * Must be called before {@link terminate}: terminating invalidates the tree, and capturing\n * an invalidated tree is refused rather than silently written. */\n public capture(witness: ResourceSignature): PersistedTree {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return captureTreeState(this.state, witness);\n }\n\n /** Installs a snapshot as this tree's state. Returns false if the snapshot was refused, in\n * which case the tree is left as it was and the open proceeds cold.\n *\n * Only meaningful before the first refresh, which is why it is private and driven from\n * {@link init}: replacing the state of a running tree would strand its observers. */\n private restore(snapshot: PersistedTree): boolean {\n if (this.sharedSeeds.length > 0) {\n this.logger?.warn(\"ignoring tree snapshot: trees with shared-type seeds are not restored\");\n return false;\n }\n\n const roots = this.currentRootSet();\n const snapshotRoots = new Set(snapshot.roots);\n if (snapshotRoots.size !== roots.size || ![...snapshotRoots].every((r) => roots.has(r))) {\n // A snapshot addressed to a different root is a mis-keyed file, not a stale one.\n this.logger?.warn(\"ignoring tree snapshot: its roots are not this tree's roots\");\n return false;\n }\n\n const restored = restoreTreeState(snapshot, this.finalPredicate, {\n roots,\n logger: this.logger,\n });\n if (restored === undefined) return false;\n\n this.state = restored;\n this.restoredFromSnapshot = true;\n return true;\n }\n\n /** Resolves the single root for the backward-compatible single-root accessors, throwing\n * if the tree does not have exactly one root (guards legacy callers against multi-root). */\n private soleRoot(): SignedResourceId {\n const roots = this.currentRootSet();\n if (roots.size !== 1)\n throw new Error(\n `single-root accessor used on a tree with ${roots.size} roots; use rootsEntry() instead`,\n );\n return roots.values().next().value!;\n }\n\n /** @deprecated use \"entry\" instead */\n public accessor(rid?: SignedResourceId): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return this.entry(rid);\n }\n\n /** Backward-compatible single-root entry. With no `rid` it returns the sole root's entry\n * and THROWS if the tree has zero or more than one root. An explicit `rid` addresses any\n * resource in the heap, as today. */\n public entry(rid?: SignedResourceId): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeEntry(\n { treeProvider: () => this.state, hooks: this.hooks },\n rid ?? this.soleRoot(),\n );\n }\n\n /** Reactive provider for the current root SET. Reading it inside a Computable tracks the\n * set as a dependency, so the Computable recomputes when discovered roots appear/disappear. */\n public rootsEntry(): PlTreeRootsEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeRootsEntry({ treeProvider: () => this.state, hooks: this.hooks });\n }\n\n /** Can be used to externally kick off the synchronization polling loop, and\n * await for the first synchronization to happen. */\n public async refreshState(): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n await this.hooks.refreshState();\n }\n\n private currentLoopDelayInterrupt: AbortController | undefined = undefined;\n private scheduledOnNextState: ScheduledRefresh[] = [];\n\n /** Interval actually used for the current wait. Starts at the configured `pollingInterval`\n * and is re-derived after every cycle by {@link updatePollingInterval}. */\n private effectivePollingInterval: number;\n\n /** Re-derives {@link effectivePollingInterval} after a cycle.\n *\n * Two independent effects. The floor scales with the client's measured RTT, so a\n * high-latency link stops queueing round-trips it cannot service. On top of that, a cycle\n * that changed nothing multiplies the interval out towards\n * {@link MAX_POLLING_INTERVAL_MS}, while any change snaps it straight back to the floor so\n * an active tree stays responsive. */\n private updatePollingInterval(changed: boolean): void {\n this.effectivePollingInterval = derivePollingInterval({\n configuredMs: this.pollingInterval,\n currentMs: this.effectivePollingInterval,\n rttMs: this.pl.rttEstimateMs,\n changed,\n });\n }\n\n /** Called from computable hooks when external observer asks for state refresh */\n private scheduleOnNextState(resolve: () => void, reject: (err: any) => void): void {\n if (this.terminated) reject(new Error(\"tree synchronization is terminated\"));\n else {\n this.scheduledOnNextState.push({ resolve, reject });\n // Someone is waiting on fresh state, so this tree is not idle after all: drop any\n // accumulated backoff, otherwise the cycles right after a nudge stay slow. Routed\n // through the policy rather than assigning the configured value directly, so the RTT\n // floor survives the reset. Assigning it raw would poll a high-latency link faster than\n // it can answer, and the interval would stay there until the next cycle that completes:\n // the error path never reaches updatePollingInterval, so a failing nudged refresh would\n // keep retrying at the un-floored rate.\n this.updatePollingInterval(true);\n if (this.currentLoopDelayInterrupt) {\n this.currentLoopDelayInterrupt.abort();\n this.currentLoopDelayInterrupt = undefined;\n }\n }\n }\n\n /** Called from observer */\n private startUpdating(): void {\n if (this.terminated) return;\n this.keepRunning = true;\n if (this.currentLoop === undefined) this.currentLoop = this.mainLoop();\n }\n\n /** Called from observer */\n private stopUpdating(): void {\n this.keepRunning = false;\n }\n\n /** If true, main loop will continue polling pl state. */\n private keepRunning = false;\n /** Actual state of main loop. */\n private currentLoop: Promise<void> | undefined = undefined;\n\n /** Executed from the main loop, and initialization procedure. */\n private async refresh(stats?: TreeLoadingStat, txOps?: TxOps): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n try {\n await this.loadAndApply(stats, txOps);\n } catch (e) {\n // Discovery-tree self-heal: a discovered root whose grant was revoked/expired fails the whole\n // ResourceTree poll with Unauthenticated. Re-discover (drops dead roots) and retry once. This is\n // self-discriminating — a genuinely dead session also fails discover()'s own call, so real auth\n // loss still propagates. Only for discovery trees; explicit-root trees propagate as-is.\n if (this.sharedSeeds.length > 0 && isUnauthenticated(e)) {\n this.logger?.warn(\n \"discovery tree: Unauthenticated on ResourceTree (likely revoked/expired root); re-discovering and retrying\",\n );\n await this.discover();\n await this.loadAndApply(stats, txOps);\n } else throw e;\n }\n }\n\n private async loadAndApply(stats?: TreeLoadingStat, txOps?: TxOps): Promise<void> {\n const request = constructTreeLoadingRequest(this.state, {\n pruningFunction: this.pruning,\n fieldFilter: this.fieldFilter,\n traverseStopRules: this.traverseStopRules,\n });\n // A shared-type-seed tree with no currently-discovered roots is legitimately empty:\n // there is nothing to traverse, and tx.resourceTree([]) would throw \"at least one seed\n // must be provided\". Skip the backend load and leave the (empty) state as-is — discovery\n // adds roots later via setRoots(), which schedules the next refresh. Explicit-root trees\n // never hit this (their root set is non-empty by construction).\n if (request.seedResources.length === 0 && request.finalResources.size === 0) return;\n const data = await this.pl.withReadTx(\n \"ReadingTree\",\n async (tx) => {\n return await loadTreeState(\n tx,\n request,\n stats,\n this.pl.serverInfo.capabilities ?? [],\n this.traversalMode,\n this.logger,\n );\n },\n txOps,\n );\n this.state.updateFromResourceData(data, { allowOrphanInputs: true, stat: stats });\n }\n\n /** Discovery sync for shared-type seeds: re-polls `ListUserResources` (gRPC-only) and\n * reconciles the discovered root set against the heap. A longer poll result adds roots; a\n * shorter one removes them (grant revoked/expired) — the removed roots' subtrees cascade\n * to collection via the ordinary refcount GC ({@link PlTreeState.setRoots}). No-op when the\n * tree has no shared-type seeds, or silently no-op on a REST client where `ListUserResources`\n * is unavailable. */\n private async discover(): Promise<void> {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n if (this.sharedSeeds.length === 0) return;\n\n const discovered = new Set<SignedResourceId>();\n for (const seed of this.sharedSeeds) {\n let ids: SignedResourceId[];\n try {\n // match by name only (permissive; ignores version, so it survives schema bumps)\n ids = await this.pl.userResources.listSharedResourcesByType(seed.resourceType.name);\n } catch (e: unknown) {\n if (isUnimplementedError(e)) continue;\n throw e;\n }\n for (const id of ids) discovered.add(id);\n }\n\n this.discoveredRoots = [...discovered];\n this.state.setRoots(this.currentRootSet());\n }\n\n /** If true this tree state is permanently terminaed. */\n private terminated = false;\n\n private async mainLoop() {\n // Always collected, even when not logging: the change counters drive the idle backoff\n // below. Counter bumps are cheap next to the round trip they describe.\n let stat = initialTreeLoadingStat();\n\n let lastUpdate = Date.now();\n\n // paces the discovery poll for shared-type seeds; 0 forces discovery on the first pass.\n let lastDiscovery = 0;\n\n while (true) {\n if (!this.keepRunning || this.terminated) break;\n\n // saving those who want to be notified about new state here\n // because those who will be added during the tree retrieval\n // should be notified only on the next round\n let toNotify: ScheduledRefresh[] | undefined = undefined;\n if (this.scheduledOnNextState.length > 0) {\n toNotify = this.scheduledOnNextState;\n this.scheduledOnNextState = [];\n }\n\n try {\n // resetting stats if we were asked to collect non-cumulative stats\n if (this.logStat === \"per-request\") stat = initialTreeLoadingStat();\n\n // discovery sync for shared-type seeds: reconcile the discovered root set before\n // refreshing, so newly discovered roots are materialized in this same iteration.\n if (this.sharedSeeds.length > 0 && Date.now() - lastDiscovery >= DISCOVERY_INTERVAL_MS) {\n await this.discover();\n lastDiscovery = Date.now();\n }\n\n // Change counters before the refresh, so the delta tells us whether this single cycle\n // brought anything new. Works in both stat modes: \"per-request\" resets to 0 above.\n const changesBefore = countedChanges(stat);\n\n // actual tree synchronization\n await this.refresh(stat);\n\n const changed = countedChanges(stat) > changesBefore;\n if (changed) this.changeGenerationCounter++;\n this.updatePollingInterval(changed);\n\n // logging stats if we were asked to\n if (this.logStat && this.logger)\n this.logger.info(\n `Tree stat (success, after ${Date.now() - lastUpdate}ms): ${JSON.stringify(stat)}`,\n );\n lastUpdate = Date.now();\n\n // notifying that we got new state\n if (toNotify !== undefined) for (const n of toNotify) n.resolve();\n } catch (e: any) {\n // logging stats if we were asked to (even if error occured)\n if (this.logStat && this.logger)\n this.logger.info(\n `Tree stat (error, after ${Date.now() - lastUpdate}ms): ${JSON.stringify(stat)}`,\n );\n lastUpdate = Date.now();\n\n // notifying that we failed to refresh the state\n if (toNotify !== undefined) for (const n of toNotify) n.reject(e);\n\n // catching tree update errors, as they may leave our tree in inconsistent state\n if (e instanceof TreeStateUpdateError) {\n // important error logging, this should never happen\n this.logger?.error(e);\n\n // marking everybody who used previous state as changed\n this.state.invalidateTree(\"stat update error\");\n // creating new tree with the full current root set (re-discovered on next iteration)\n this.state = new PlTreeState(this.currentRootSet(), this.finalPredicate);\n\n // scheduling state update without delay\n continue;\n\n // unfortunately external observer may still see tree in its default\n // empty state, though this is best we can do in this exceptional\n // situation, and hope on caching layers inside computables to present\n // some stale state until we reconstruct the tree again\n } else this.logger?.warn(e);\n }\n\n if (!this.keepRunning || this.terminated) break;\n\n // Phase 1: mandatory floor — always wait at least MIN_POLLING_INTERVAL_MS.\n // Not interruptible by scheduleOnNextState; only termination aborts it.\n try {\n await tp.setTimeout(MIN_POLLING_INTERVAL_MS, undefined, {\n signal: this.abortController.signal,\n });\n } catch (e: unknown) {\n if (!isTimeoutOrCancelError(e)) throw new Error(\"Unexpected error\", { cause: e });\n if (this.abortController.signal.aborted) break;\n }\n\n if (!this.keepRunning || this.terminated) break;\n\n // Phase 2: optional remainder up to pollingInterval — interruptible by\n // scheduleOnNextState so that an external nudge wakes the loop promptly.\n if (this.scheduledOnNextState.length === 0) {\n const remaining = Math.max(0, this.effectivePollingInterval - MIN_POLLING_INTERVAL_MS);\n if (remaining > 0) {\n try {\n this.currentLoopDelayInterrupt = new AbortController();\n await tp.setTimeout(remaining, undefined, {\n signal: AbortSignal.any([\n this.abortController.signal,\n this.currentLoopDelayInterrupt.signal,\n ]),\n });\n } catch (e: unknown) {\n if (!isTimeoutOrCancelError(e)) throw new Error(\"Unexpected error\", { cause: e });\n if (this.abortController.signal.aborted) break;\n // Otherwise it was just the loop delay interrupt (scheduleOnNextState),\n // continue to the next iteration\n } finally {\n this.currentLoopDelayInterrupt = undefined;\n }\n }\n }\n }\n\n // reset only as a very last line\n this.currentLoop = undefined;\n }\n\n /**\n * Dumps the current state of the tree.\n * @returns An array of ExtendedResourceData objects representing the current state of the tree.\n */\n public dumpState(): ExtendedResourceData[] {\n return this.state.dumpState();\n }\n\n /**\n * Terminates the internal loop, and permanently destoys all internal state, so\n * all computables using this state will resolve to errors.\n * */\n public async terminate(): Promise<void> {\n this.keepRunning = false;\n this.terminated = true;\n this.abortController.abort();\n\n if (this.currentLoop === undefined) return;\n await this.currentLoop;\n\n this.state.invalidateTree(\"synchronization terminated for the tree\");\n }\n\n /** @deprecated */\n public async awaitSyncLoopTermination(): Promise<void> {\n if (this.currentLoop === undefined) return;\n await this.currentLoop;\n }\n\n /**\n * Initializes a synchronized tree from one or more seeds.\n *\n * @param seeds a bare {@link SignedResourceId} (the original single-root contract), a\n * single {@link TreeSeed}, or an array of seeds. Bare ids and explicit-resource seeds\n * become roots immediately; shared-type seeds discover their roots via `ListUserResources`.\n */\n public static async init(\n pl: PlClient,\n seeds: SignedResourceId | TreeSeed | TreeSeed[],\n ops: SynchronizedTreeOps,\n logger?: MiLogger,\n ) {\n const tree = new SynchronizedTreeState(pl, normalizeSeeds(seeds), ops, logger);\n\n // Seed from the snapshot before the first refresh, so that refresh is the one that\n // transfers only what changed. A refused snapshot leaves an ordinary cold open.\n const restored = ops.restoreFrom !== undefined && tree.restore(ops.restoreFrom);\n\n // Always collected, even when not logging: the initial load's change count is what seeds\n // the change generation, so a holder can tell a populated tree from an untouched one.\n const stat = initialTreeLoadingStat();\n\n let ok = false;\n\n try {\n // resolve shared-type seeds before the first refresh so discovered roots load now\n await tree.discover();\n await tree.refresh(stat, {\n timeout: ops.initialTreeLoadingTimeout,\n });\n ok = true;\n } finally {\n if (countedChanges(stat) > 0) tree.changeGenerationCounter++;\n\n // logging stats if we were asked to (even if error occured)\n if (ops.logStat && logger)\n logger.info(\n `Tree stat (initial load, ${ok ? \"success\" : \"failure\"}, ${\n restored ? \"restored from snapshot\" : \"cold\"\n }): ${JSON.stringify(stat)}`,\n );\n }\n\n return tree;\n }\n}\n"],"mappings":";;;;;;;;;;;AA4BA,MAAM,0BAA0B;;;AAIhC,MAAM,0BAA0B;;;;AAKhC,MAAM,0BAA0B;;;;;AAMhC,MAAM,kBAAkB;;;;;AAMxB,MAAM,2BAA2B;;;;AA0DjC,SAAS,eAAe,OAA6D;CACnF,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CACjC,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,OAAO,OAAO,CAAC,KAAK;CAEjF,OAAO,CAAC;EAAE,MAAM;EAAY,MAAM;CAAM,CAAC;AAC3C;;;;;;;;;;;;;;AAeA,MAAM,wBAAwB;;;;;AAM9B,SAAS,eAAe,MAA+B;CAOrD,OAAO,KAAK,eAAe,KAAK,mBAAmB,KAAK,uBAAuB,KAAK;AACtF;;;;;;;AAQA,SAAgB,sBAAsB,MAK3B;CACT,MAAM,EAAE,cAAc,WAAW,OAAO,YAAY;CAIpD,MAAM,QACJ,UAAU,KAAA,IACN,eACA,KAAK,IACH,cACA,KAAK,IAAI,0BAA0B,KAAK,KAAK,QAAQ,eAAe,CAAC,CACvE;CAEN,MAAM,OAAO,UAAU,QAAQ,KAAK,IAAI,OAAO,YAAY,uBAAuB;CAIlF,OAAO,KAAK,IAAI,OAAO,KAAK,IAAI,yBAAyB,IAAI,CAAC;AAChE;AAOA,IAAa,wBAAb,MAAa,sBAAsB;CA6Bd;CAGA;CA/BnB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,kBAAmC,IAAI,gBAAgB;;CAGvD;;CAEA;;CAEA,kBAA8C,CAAC;;;;CAK/C,0BAAkC;;;CAIlC,uBAA+B;CAE/B,YACE,IACA,OACA,KACA,QACA;EAJiB,KAAA,KAAA;EAGA,KAAA,SAAA;EAEjB,MAAM,EACJ,wBACA,SACA,aACA,mBACA,eACA,iBACA,kBACA,YACE;EACJ,KAAK,UAAU;EACf,KAAK,cAAc;EACnB,KAAK,oBAAoB;EACzB,KAAK,gBAAgB,iBAAiB;EACtC,KAAK,kBAAkB;EACvB,KAAK,2BAA2B;EAChC,KAAK,iBAAiB,0BAA0B,GAAG;EACnD,KAAK,UAAU;EAEf,KAAK,gBAAgB,MAClB,QAAQ,MAA6B,EAAE,SAAS,UAAU,CAAC,CAC3D,KAAK,MAAM,EAAE,IAAI;EACpB,KAAK,cAAc,MAAM,QAAQ,MAA2B,EAAE,SAAS,QAAQ;EAE/E,KAAK,QAAQ,IAAI,YAAY,KAAK,eAAe,GAAG,KAAK,cAAc;EACvE,KAAK,QAAQ,IAAI,6BACT,KAAK,cAAc,SACnB,KAAK,aAAa,GACxB,EAAE,cAAc,iBAAiB,IAChC,SAAS,WAAW,KAAK,oBAAoB,SAAS,MAAM,CAC/D;CACF;;CAGA,iBAAgD;EAC9C,uBAAO,IAAI,IAAI,CAAC,GAAG,KAAK,eAAe,GAAG,KAAK,eAAe,CAAC;CACjE;;;;CAKA,IAAW,mBAA2B;EACpC,OAAO,KAAK;CACd;;;;;CAMA,IAAW,0BAAmC;EAC5C,OAAO,KAAK;CACd;;;;;CAMA,QAAe,SAA2C;EACxD,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,OAAO,iBAAiB,KAAK,OAAO,OAAO;CAC7C;;;;;;CAOA,QAAgB,UAAkC;EAChD,IAAI,KAAK,YAAY,SAAS,GAAG;GAC/B,KAAK,QAAQ,KAAK,uEAAuE;GACzF,OAAO;EACT;EAEA,MAAM,QAAQ,KAAK,eAAe;EAClC,MAAM,gBAAgB,IAAI,IAAI,SAAS,KAAK;EAC5C,IAAI,cAAc,SAAS,MAAM,QAAQ,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,OAAO,MAAM,MAAM,IAAI,CAAC,CAAC,GAAG;GAEvF,KAAK,QAAQ,KAAK,6DAA6D;GAC/E,OAAO;EACT;EAEA,MAAM,WAAW,iBAAiB,UAAU,KAAK,gBAAgB;GAC/D;GACA,QAAQ,KAAK;EACf,CAAC;EACD,IAAI,aAAa,KAAA,GAAW,OAAO;EAEnC,KAAK,QAAQ;EACb,KAAK,uBAAuB;EAC5B,OAAO;CACT;;;CAIA,WAAqC;EACnC,MAAM,QAAQ,KAAK,eAAe;EAClC,IAAI,MAAM,SAAS,GACjB,MAAM,IAAI,MACR,4CAA4C,MAAM,KAAK,iCACzD;EACF,OAAO,MAAM,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC;CAC/B;;CAGA,SAAgB,KAAqC;EACnD,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,OAAO,KAAK,MAAM,GAAG;CACvB;;;;CAKA,MAAa,KAAqC;EAChD,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,OAAO,IAAI,YACT;GAAE,oBAAoB,KAAK;GAAO,OAAO,KAAK;EAAM,GACpD,OAAO,KAAK,SAAS,CACvB;CACF;;;CAIA,aAAsC;EACpC,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,OAAO,IAAI,iBAAiB;GAAE,oBAAoB,KAAK;GAAO,OAAO,KAAK;EAAM,CAAC;CACnF;;;CAIA,MAAa,eAA8B;EACzC,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,MAAM,KAAK,MAAM,aAAa;CAChC;CAEA,4BAAiE,KAAA;CACjE,uBAAmD,CAAC;;;CAIpD;;;;;;;;CASA,sBAA8B,SAAwB;EACpD,KAAK,2BAA2B,sBAAsB;GACpD,cAAc,KAAK;GACnB,WAAW,KAAK;GAChB,OAAO,KAAK,GAAG;GACf;EACF,CAAC;CACH;;CAGA,oBAA4B,SAAqB,QAAkC;EACjF,IAAI,KAAK,YAAY,uBAAO,IAAI,MAAM,oCAAoC,CAAC;OACtE;GACH,KAAK,qBAAqB,KAAK;IAAE;IAAS;GAAO,CAAC;GAQlD,KAAK,sBAAsB,IAAI;GAC/B,IAAI,KAAK,2BAA2B;IAClC,KAAK,0BAA0B,MAAM;IACrC,KAAK,4BAA4B,KAAA;GACnC;EACF;CACF;;CAGA,gBAA8B;EAC5B,IAAI,KAAK,YAAY;EACrB,KAAK,cAAc;EACnB,IAAI,KAAK,gBAAgB,KAAA,GAAW,KAAK,cAAc,KAAK,SAAS;CACvE;;CAGA,eAA6B;EAC3B,KAAK,cAAc;CACrB;;CAGA,cAAsB;;CAEtB,cAAiD,KAAA;;CAGjD,MAAc,QAAQ,OAAyB,OAA8B;EAC3E,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,IAAI;GACF,MAAM,KAAK,aAAa,OAAO,KAAK;EACtC,SAAS,GAAG;GAKV,IAAI,KAAK,YAAY,SAAS,KAAK,kBAAkB,CAAC,GAAG;IACvD,KAAK,QAAQ,KACX,4GACF;IACA,MAAM,KAAK,SAAS;IACpB,MAAM,KAAK,aAAa,OAAO,KAAK;GACtC,OAAO,MAAM;EACf;CACF;CAEA,MAAc,aAAa,OAAyB,OAA8B;EAChF,MAAM,UAAU,4BAA4B,KAAK,OAAO;GACtD,iBAAiB,KAAK;GACtB,aAAa,KAAK;GAClB,mBAAmB,KAAK;EAC1B,CAAC;EAMD,IAAI,QAAQ,cAAc,WAAW,KAAK,QAAQ,eAAe,SAAS,GAAG;EAC7E,MAAM,OAAO,MAAM,KAAK,GAAG,WACzB,eACA,OAAO,OAAO;GACZ,OAAO,MAAM,cACX,IACA,SACA,OACA,KAAK,GAAG,WAAW,gBAAgB,CAAC,GACpC,KAAK,eACL,KAAK,MACP;EACF,GACA,KACF;EACA,KAAK,MAAM,uBAAuB,MAAM;GAAE,mBAAmB;GAAM,MAAM;EAAM,CAAC;CAClF;;;;;;;CAQA,MAAc,WAA0B;EACtC,IAAI,KAAK,YAAY,MAAM,IAAI,MAAM,oCAAoC;EACzE,IAAI,KAAK,YAAY,WAAW,GAAG;EAEnC,MAAM,6BAAa,IAAI,IAAsB;EAC7C,KAAK,MAAM,QAAQ,KAAK,aAAa;GACnC,IAAI;GACJ,IAAI;IAEF,MAAM,MAAM,KAAK,GAAG,cAAc,0BAA0B,KAAK,aAAa,IAAI;GACpF,SAAS,GAAY;IACnB,IAAI,qBAAqB,CAAC,GAAG;IAC7B,MAAM;GACR;GACA,KAAK,MAAM,MAAM,KAAK,WAAW,IAAI,EAAE;EACzC;EAEA,KAAK,kBAAkB,CAAC,GAAG,UAAU;EACrC,KAAK,MAAM,SAAS,KAAK,eAAe,CAAC;CAC3C;;CAGA,aAAqB;CAErB,MAAc,WAAW;EAGvB,IAAI,OAAO,uBAAuB;EAElC,IAAI,aAAa,KAAK,IAAI;EAG1B,IAAI,gBAAgB;EAEpB,OAAO,MAAM;GACX,IAAI,CAAC,KAAK,eAAe,KAAK,YAAY;GAK1C,IAAI,WAA2C,KAAA;GAC/C,IAAI,KAAK,qBAAqB,SAAS,GAAG;IACxC,WAAW,KAAK;IAChB,KAAK,uBAAuB,CAAC;GAC/B;GAEA,IAAI;IAEF,IAAI,KAAK,YAAY,eAAe,OAAO,uBAAuB;IAIlE,IAAI,KAAK,YAAY,SAAS,KAAK,KAAK,IAAI,IAAI,iBAAiB,uBAAuB;KACtF,MAAM,KAAK,SAAS;KACpB,gBAAgB,KAAK,IAAI;IAC3B;IAIA,MAAM,gBAAgB,eAAe,IAAI;IAGzC,MAAM,KAAK,QAAQ,IAAI;IAEvB,MAAM,UAAU,eAAe,IAAI,IAAI;IACvC,IAAI,SAAS,KAAK;IAClB,KAAK,sBAAsB,OAAO;IAGlC,IAAI,KAAK,WAAW,KAAK,QACvB,KAAK,OAAO,KACV,6BAA6B,KAAK,IAAI,IAAI,WAAW,OAAO,KAAK,UAAU,IAAI,GACjF;IACF,aAAa,KAAK,IAAI;IAGtB,IAAI,aAAa,KAAA,GAAW,KAAK,MAAM,KAAK,UAAU,EAAE,QAAQ;GAClE,SAAS,GAAQ;IAEf,IAAI,KAAK,WAAW,KAAK,QACvB,KAAK,OAAO,KACV,2BAA2B,KAAK,IAAI,IAAI,WAAW,OAAO,KAAK,UAAU,IAAI,GAC/E;IACF,aAAa,KAAK,IAAI;IAGtB,IAAI,aAAa,KAAA,GAAW,KAAK,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;IAGhE,IAAI,aAAa,sBAAsB;KAErC,KAAK,QAAQ,MAAM,CAAC;KAGpB,KAAK,MAAM,eAAe,mBAAmB;KAE7C,KAAK,QAAQ,IAAI,YAAY,KAAK,eAAe,GAAG,KAAK,cAAc;KAGvE;IAMF,OAAO,KAAK,QAAQ,KAAK,CAAC;GAC5B;GAEA,IAAI,CAAC,KAAK,eAAe,KAAK,YAAY;GAI1C,IAAI;IACF,MAAM,GAAG,WAAW,yBAAyB,KAAA,GAAW,EACtD,QAAQ,KAAK,gBAAgB,OAC/B,CAAC;GACH,SAAS,GAAY;IACnB,IAAI,CAAC,uBAAuB,CAAC,GAAG,MAAM,IAAI,MAAM,oBAAoB,EAAE,OAAO,EAAE,CAAC;IAChF,IAAI,KAAK,gBAAgB,OAAO,SAAS;GAC3C;GAEA,IAAI,CAAC,KAAK,eAAe,KAAK,YAAY;GAI1C,IAAI,KAAK,qBAAqB,WAAW,GAAG;IAC1C,MAAM,YAAY,KAAK,IAAI,GAAG,KAAK,2BAA2B,uBAAuB;IACrF,IAAI,YAAY,GACd,IAAI;KACF,KAAK,4BAA4B,IAAI,gBAAgB;KACrD,MAAM,GAAG,WAAW,WAAW,KAAA,GAAW,EACxC,QAAQ,YAAY,IAAI,CACtB,KAAK,gBAAgB,QACrB,KAAK,0BAA0B,MACjC,CAAC,EACH,CAAC;IACH,SAAS,GAAY;KACnB,IAAI,CAAC,uBAAuB,CAAC,GAAG,MAAM,IAAI,MAAM,oBAAoB,EAAE,OAAO,EAAE,CAAC;KAChF,IAAI,KAAK,gBAAgB,OAAO,SAAS;IAG3C,UAAU;KACR,KAAK,4BAA4B,KAAA;IACnC;GAEJ;EACF;EAGA,KAAK,cAAc,KAAA;CACrB;;;;;CAMA,YAA2C;EACzC,OAAO,KAAK,MAAM,UAAU;CAC9B;;;;;CAMA,MAAa,YAA2B;EACtC,KAAK,cAAc;EACnB,KAAK,aAAa;EAClB,KAAK,gBAAgB,MAAM;EAE3B,IAAI,KAAK,gBAAgB,KAAA,GAAW;EACpC,MAAM,KAAK;EAEX,KAAK,MAAM,eAAe,yCAAyC;CACrE;;CAGA,MAAa,2BAA0C;EACrD,IAAI,KAAK,gBAAgB,KAAA,GAAW;EACpC,MAAM,KAAK;CACb;;;;;;;;CASA,aAAoB,KAClB,IACA,OACA,KACA,QACA;EACA,MAAM,OAAO,IAAI,sBAAsB,IAAI,eAAe,KAAK,GAAG,KAAK,MAAM;EAI7E,MAAM,WAAW,IAAI,gBAAgB,KAAA,KAAa,KAAK,QAAQ,IAAI,WAAW;EAI9E,MAAM,OAAO,uBAAuB;EAEpC,IAAI,KAAK;EAET,IAAI;GAEF,MAAM,KAAK,SAAS;GACpB,MAAM,KAAK,QAAQ,MAAM,EACvB,SAAS,IAAI,0BACf,CAAC;GACD,KAAK;EACP,UAAU;GACR,IAAI,eAAe,IAAI,IAAI,GAAG,KAAK;GAGnC,IAAI,IAAI,WAAW,QACjB,OAAO,KACL,4BAA4B,KAAK,YAAY,UAAU,IACrD,WAAW,2BAA2B,OACvC,KAAK,KAAK,UAAU,IAAI,GAC3B;EACJ;EAEA,OAAO;CACT;AACF"}
|