@milaboratories/pl-tree 1.12.15 → 1.12.17
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.
|
@@ -177,7 +177,13 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
177
177
|
if (this.sharedSeeds.length === 0) return;
|
|
178
178
|
const discovered = /* @__PURE__ */ new Set();
|
|
179
179
|
for (const seed of this.sharedSeeds) {
|
|
180
|
-
|
|
180
|
+
let ids;
|
|
181
|
+
try {
|
|
182
|
+
ids = await this.pl.userResources.listSharedResourcesByType(seed.resourceType.name);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if ((0, _milaboratories_pl_client.isUnimplementedError)(e)) continue;
|
|
185
|
+
throw e;
|
|
186
|
+
}
|
|
181
187
|
for (const id of ids) discovered.add(id);
|
|
182
188
|
}
|
|
183
189
|
this.discoveredRoots = [...discovered];
|
|
@@ -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 { isUnauthenticated, isTimeoutOrCancelError } 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\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, in main-loop iterations, a tree with shared-type seeds re-polls\n * ListUserResources to reconcile its discovered roots. 1 would mean every iteration.\n *\n * Discovery (a full ListUserResources stream) is far heavier than an ordinary incremental\n * refresh, so it must NOT run on every fast refresh tick. The refresh cadence is the tree's\n * `pollingInterval` floored by {@link MIN_POLLING_INTERVAL_MS} (~200ms-1s in practice — the\n * shared-seed discovery tree runs at the 200ms default). At N = 15 discovery fires roughly\n * every 15 × 200ms ≈ 3s, decoupling it from the fast refresh loop while keeping the latency\n * of noticing a new/removed share to a few seconds — acceptable for a human-driven share flow.\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_EVERY_N_REFRESHES = 15;\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.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 /** 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 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, true);\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 // match by name only (permissive; ignores version, so it survives schema bumps)\n const ids = await this.pl.userResources.listSharedResourcesByType(seed.resourceType.name);\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 // will hold request stats\n let stat = this.logStat ? initialTreeLoadingStat() : undefined;\n\n let lastUpdate = Date.now();\n\n // counts refresh iterations to pace the discovery poll for shared-type seeds.\n let iteration = 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 && iteration % DISCOVERY_EVERY_N_REFRESHES === 0) {\n await this.discover();\n }\n iteration++;\n\n // actual tree synchronization\n await this.refresh(stat);\n\n // logging stats if we were asked to\n if (stat && 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 (stat && 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.pollingInterval - 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","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\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, in main-loop iterations, a tree with shared-type seeds re-polls\n * ListUserResources to reconcile its discovered roots. 1 would mean every iteration.\n *\n * Discovery (a full ListUserResources stream) is far heavier than an ordinary incremental\n * refresh, so it must NOT run on every fast refresh tick. The refresh cadence is the tree's\n * `pollingInterval` floored by {@link MIN_POLLING_INTERVAL_MS} (~200ms-1s in practice — the\n * shared-seed discovery tree runs at the 200ms default). At N = 15 discovery fires roughly\n * every 15 × 200ms ≈ 3s, decoupling it from the fast refresh loop while keeping the latency\n * of noticing a new/removed share to a few seconds — acceptable for a human-driven share flow.\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_EVERY_N_REFRESHES = 15;\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.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 /** 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 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, true);\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 // will hold request stats\n let stat = this.logStat ? initialTreeLoadingStat() : undefined;\n\n let lastUpdate = Date.now();\n\n // counts refresh iterations to pace the discovery poll for shared-type seeds.\n let iteration = 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 && iteration % DISCOVERY_EVERY_N_REFRESHES === 0) {\n await this.discover();\n }\n iteration++;\n\n // actual tree synchronization\n await this.refresh(stat);\n\n // logging stats if we were asked to\n if (stat && 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 (stat && 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.pollingInterval - 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 +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":";;;;;;;KA2BK,eAAA;AAAA,KAEO,mBAAA;EAFQ,iDAIlB,sBAAA,GAAyB,0BAAA,EAJP;EAOlB,OAAA,GAAU,eAAA,EALA;EAQV,WAAA,GAAc,MAAA;EAGd,iBAAA,GAAoB,MAAA,EANV;EASV,eAAA,UAHoB;EAKpB,gBAAA,UASgB;EANhB,OAAA,GAAU,eAAA,EAMmB;EAH7B,yBAAA,WApByB;EAuBzB,aAAA,GAAgB,aAAA;AAAA;;KAIN,gBAAA;EAAqB,IAAA;EAAkB,IAAA,EAAM,gBAAgB;AAAA;;;;KAK7D,cAAA;EAAmB,IAAA;EAAgB,YAAA,EAAc,YAAY;AAAA;AAAA,KAE7D,QAAA,GAAW,gBAAA,GAAmB,cAAc;AAAA,cA+B3C,qBAAA;EAAA,iBAoBQ,EAAA;EAAA,iBAGA,MAAA;EAAA,iBAtBF,cAAA;EAAA,QACT,KAAA;EAAA,iBACS,eAAA;EAAA,iBACA,OAAA;EAAA,iBACA,WAAA;EAAA,iBACA,iBAAA;EAAA,iBACA,aAAA;EAAA,iBACA,OAAA;EAAA,iBACA,KAAA;EAAA,iBACA,eAAA;EA3CY;EAAA,iBA8CZ,aAAA;EA9C0C;EAAA,iBAgD1C,WAAA;EAhDsD;EAAA,QAkD/D,eAAA;EAAA,QAED,WAAA;;UAuCC,cAAA;EAzF8C;AA+BxD;EA/BwD,QA+F9C,QAAA;;EAUD,QAAA,CAAS,GAAA,GAAM,gBAAA,GAAmB,WAAA;EAAA;;;EAQlC,KAAA,CAAM,GAAA,GAAM,gBAAA,GAAmB,WAAA;EAiBT;;EAPtB,UAAA,IAAc,gBAAA;EA0QoB;;EAnQ5B,YAAA,IAAgB,OAAA;EAAA,QAKrB,yBAAA;EAAA,QACA,oBAAA;EA4QD;EAAA,QAzQC,mBAAA;EA0QW;EAAA,QA9PX,aAAA;EA8PW;EAAA,QAvPX,YAAA;EA3GW;EAAA,QAgHX,WAAA;EAnIS;EAAA,QAqIT,WAAA;EAnIS;EAAA,QAsIH,OAAA;EAAA,QAmBA,YAAA;EAtJG;;;;;;EAAA,QAyLH,QAAA;EA9KN;EAAA,QAoMA,UAAA;EAAA,QAEM,QAAA;EAvJN;;;;EAkRD,SAAA,IAAa,oBAAA;EAhQb;;;;EAwQM,SAAA,IAAa,OAAA;EA9PL;EA0QR,wBAAA,IAA4B,OAAA;EAnQZ;;;;;;;EAAA,OA+QT,IAAA,CAClB,EAAA,EAAI,QAAA,EACJ,KAAA,EAAO,gBAAA,GAAmB,QAAA,GAAW,QAAA,IACrC,GAAA,EAAK,mBAAA,EACL,MAAA,GAAS,QAAA,GAAQ,OAAA,CAAA,qBAAA;AAAA"}
|
|
@@ -1,7 +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 { isTimeoutOrCancelError, isUnauthenticated } from "@milaboratories/pl-client";
|
|
4
|
+
import { isTimeoutOrCancelError, isUnauthenticated, isUnimplementedError } from "@milaboratories/pl-client";
|
|
5
5
|
import { PollingComputableHooks } from "@milaboratories/computable";
|
|
6
6
|
import * as tp from "node:timers/promises";
|
|
7
7
|
//#region src/synchronized_tree.ts
|
|
@@ -175,7 +175,13 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
175
175
|
if (this.sharedSeeds.length === 0) return;
|
|
176
176
|
const discovered = /* @__PURE__ */ new Set();
|
|
177
177
|
for (const seed of this.sharedSeeds) {
|
|
178
|
-
|
|
178
|
+
let ids;
|
|
179
|
+
try {
|
|
180
|
+
ids = await this.pl.userResources.listSharedResourcesByType(seed.resourceType.name);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
if (isUnimplementedError(e)) continue;
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
179
185
|
for (const id of ids) discovered.add(id);
|
|
180
186
|
}
|
|
181
187
|
this.discoveredRoots = [...discovered];
|
|
@@ -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 { isUnauthenticated, isTimeoutOrCancelError } 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\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, in main-loop iterations, a tree with shared-type seeds re-polls\n * ListUserResources to reconcile its discovered roots. 1 would mean every iteration.\n *\n * Discovery (a full ListUserResources stream) is far heavier than an ordinary incremental\n * refresh, so it must NOT run on every fast refresh tick. The refresh cadence is the tree's\n * `pollingInterval` floored by {@link MIN_POLLING_INTERVAL_MS} (~200ms-1s in practice — the\n * shared-seed discovery tree runs at the 200ms default). At N = 15 discovery fires roughly\n * every 15 × 200ms ≈ 3s, decoupling it from the fast refresh loop while keeping the latency\n * of noticing a new/removed share to a few seconds — acceptable for a human-driven share flow.\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_EVERY_N_REFRESHES = 15;\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.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 /** 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 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, true);\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 // match by name only (permissive; ignores version, so it survives schema bumps)\n const ids = await this.pl.userResources.listSharedResourcesByType(seed.resourceType.name);\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 // will hold request stats\n let stat = this.logStat ? initialTreeLoadingStat() : undefined;\n\n let lastUpdate = Date.now();\n\n // counts refresh iterations to pace the discovery poll for shared-type seeds.\n let iteration = 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 && iteration % DISCOVERY_EVERY_N_REFRESHES === 0) {\n await this.discover();\n }\n iteration++;\n\n // actual tree synchronization\n await this.refresh(stat);\n\n // logging stats if we were asked to\n if (stat && 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 (stat && 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.pollingInterval - 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 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\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, in main-loop iterations, a tree with shared-type seeds re-polls\n * ListUserResources to reconcile its discovered roots. 1 would mean every iteration.\n *\n * Discovery (a full ListUserResources stream) is far heavier than an ordinary incremental\n * refresh, so it must NOT run on every fast refresh tick. The refresh cadence is the tree's\n * `pollingInterval` floored by {@link MIN_POLLING_INTERVAL_MS} (~200ms-1s in practice — the\n * shared-seed discovery tree runs at the 200ms default). At N = 15 discovery fires roughly\n * every 15 × 200ms ≈ 3s, decoupling it from the fast refresh loop while keeping the latency\n * of noticing a new/removed share to a few seconds — acceptable for a human-driven share flow.\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_EVERY_N_REFRESHES = 15;\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.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 /** 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 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, true);\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 // will hold request stats\n let stat = this.logStat ? initialTreeLoadingStat() : undefined;\n\n let lastUpdate = Date.now();\n\n // counts refresh iterations to pace the discovery poll for shared-type seeds.\n let iteration = 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 && iteration % DISCOVERY_EVERY_N_REFRESHES === 0) {\n await this.discover();\n }\n iteration++;\n\n // actual tree synchronization\n await this.refresh(stat);\n\n // logging stats if we were asked to\n if (stat && 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 (stat && 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.pollingInterval - 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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-tree",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.17",
|
|
4
4
|
"description": "Reactive pl tree state",
|
|
5
5
|
"files": [
|
|
6
6
|
"./dist/**/*",
|
|
@@ -21,19 +21,19 @@
|
|
|
21
21
|
"denque": "^2.1.0",
|
|
22
22
|
"utility-types": "^3.11.0",
|
|
23
23
|
"zod": "~3.25.76",
|
|
24
|
-
"@milaboratories/ts-helpers": "1.8.3",
|
|
25
|
-
"@milaboratories/pl-client": "3.12.1",
|
|
26
24
|
"@milaboratories/computable": "2.9.5",
|
|
27
|
-
"@milaboratories/pl-errors": "1.4.
|
|
25
|
+
"@milaboratories/pl-errors": "1.4.27",
|
|
26
|
+
"@milaboratories/ts-helpers": "1.8.3",
|
|
27
|
+
"@milaboratories/pl-client": "3.13.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "~24.5.2",
|
|
31
31
|
"@vitest/coverage-istanbul": "^4.1.3",
|
|
32
32
|
"typescript": "~5.9.3",
|
|
33
33
|
"vitest": "^4.1.3",
|
|
34
|
-
"@milaboratories/build-configs": "2.0.0",
|
|
35
34
|
"@milaboratories/ts-builder": "1.6.0",
|
|
36
|
-
"@milaboratories/ts-configs": "1.3.0"
|
|
35
|
+
"@milaboratories/ts-configs": "1.3.0",
|
|
36
|
+
"@milaboratories/build-configs": "2.0.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=22.19.0"
|
package/src/synchronized_tree.ts
CHANGED
|
@@ -8,7 +8,11 @@ import type {
|
|
|
8
8
|
TxOps,
|
|
9
9
|
} from "@milaboratories/pl-client";
|
|
10
10
|
import type { Filter } from "@milaboratories/pl-client";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
isUnauthenticated,
|
|
13
|
+
isTimeoutOrCancelError,
|
|
14
|
+
isUnimplementedError,
|
|
15
|
+
} from "@milaboratories/pl-client";
|
|
12
16
|
import type { ExtendedResourceData } from "./state";
|
|
13
17
|
import { PlTreeState, TreeStateUpdateError } from "./state";
|
|
14
18
|
import type { PruningFunction, TraversalMode, TreeLoadingStat } from "./sync";
|
|
@@ -287,8 +291,14 @@ export class SynchronizedTreeState {
|
|
|
287
291
|
|
|
288
292
|
const discovered = new Set<SignedResourceId>();
|
|
289
293
|
for (const seed of this.sharedSeeds) {
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
let ids: SignedResourceId[];
|
|
295
|
+
try {
|
|
296
|
+
// match by name only (permissive; ignores version, so it survives schema bumps)
|
|
297
|
+
ids = await this.pl.userResources.listSharedResourcesByType(seed.resourceType.name);
|
|
298
|
+
} catch (e: unknown) {
|
|
299
|
+
if (isUnimplementedError(e)) continue;
|
|
300
|
+
throw e;
|
|
301
|
+
}
|
|
292
302
|
for (const id of ids) discovered.add(id);
|
|
293
303
|
}
|
|
294
304
|
|