@milaboratories/pl-tree 1.10.6 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -2
- package/dist/sync.cjs +112 -19
- package/dist/sync.cjs.map +1 -1
- package/dist/sync.d.ts +23 -10
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +112 -19
- package/dist/sync.js.map +1 -1
- package/dist/synchronized_tree.cjs +32 -8
- package/dist/synchronized_tree.cjs.map +1 -1
- package/dist/synchronized_tree.d.ts +11 -7
- package/dist/synchronized_tree.d.ts.map +1 -1
- package/dist/synchronized_tree.js +32 -8
- package/dist/synchronized_tree.js.map +1 -1
- package/package.json +7 -7
- package/src/sync.test.ts +443 -2
- package/src/sync.ts +202 -29
- package/src/synchronized_tree.ts +79 -22
|
@@ -7,11 +7,18 @@ let _milaboratories_computable = require("@milaboratories/computable");
|
|
|
7
7
|
let node_timers_promises = require("node:timers/promises");
|
|
8
8
|
node_timers_promises = require_runtime.__toESM(node_timers_promises);
|
|
9
9
|
//#region src/synchronized_tree.ts
|
|
10
|
+
/** Hard floor between consecutive tree-refresh calls.
|
|
11
|
+
* Applies even when {@link scheduleOnNextState} has woken the loop early,
|
|
12
|
+
* preventing tight polling loops during rapid state transitions. */
|
|
13
|
+
const MIN_POLLING_INTERVAL_MS = 100;
|
|
10
14
|
var SynchronizedTreeState = class SynchronizedTreeState {
|
|
11
15
|
finalPredicate;
|
|
12
16
|
state;
|
|
13
17
|
pollingInterval;
|
|
14
18
|
pruning;
|
|
19
|
+
fieldFilter;
|
|
20
|
+
traverseStopRules;
|
|
21
|
+
traversalMode;
|
|
15
22
|
logStat;
|
|
16
23
|
hooks;
|
|
17
24
|
abortController = new AbortController();
|
|
@@ -19,8 +26,11 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
19
26
|
this.pl = pl;
|
|
20
27
|
this.root = root;
|
|
21
28
|
this.logger = logger;
|
|
22
|
-
const { finalPredicateOverride, pruning, pollingInterval, stopPollingDelay, logStat } = ops;
|
|
29
|
+
const { finalPredicateOverride, pruning, fieldFilter, traverseStopRules, traversalMode, pollingInterval, stopPollingDelay, logStat } = ops;
|
|
23
30
|
this.pruning = pruning;
|
|
31
|
+
this.fieldFilter = fieldFilter;
|
|
32
|
+
this.traverseStopRules = traverseStopRules;
|
|
33
|
+
this.traversalMode = traversalMode ?? "auto";
|
|
24
34
|
this.pollingInterval = pollingInterval;
|
|
25
35
|
this.finalPredicate = finalPredicateOverride ?? pl.finalPredicate;
|
|
26
36
|
this.logStat = logStat;
|
|
@@ -78,9 +88,13 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
78
88
|
/** Executed from the main loop, and initialization procedure. */
|
|
79
89
|
async refresh(stats, txOps) {
|
|
80
90
|
if (this.terminated) throw new Error("tree synchronization is terminated");
|
|
81
|
-
const request = require_sync.constructTreeLoadingRequest(this.state,
|
|
91
|
+
const request = require_sync.constructTreeLoadingRequest(this.state, {
|
|
92
|
+
pruningFunction: this.pruning,
|
|
93
|
+
fieldFilter: this.fieldFilter,
|
|
94
|
+
traverseStopRules: this.traverseStopRules
|
|
95
|
+
});
|
|
82
96
|
const data = await this.pl.withReadTx("ReadingTree", async (tx) => {
|
|
83
|
-
return await require_sync.loadTreeState(tx, request, stats);
|
|
97
|
+
return await require_sync.loadTreeState(tx, request, stats, this.pl.serverInfo.capabilities ?? [], this.traversalMode, this.logger);
|
|
84
98
|
}, txOps);
|
|
85
99
|
this.state.updateFromResourceData(data, true);
|
|
86
100
|
}
|
|
@@ -114,14 +128,24 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
114
128
|
} else this.logger?.warn(e);
|
|
115
129
|
}
|
|
116
130
|
if (!this.keepRunning || this.terminated) break;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
await node_timers_promises.setTimeout(this.pollingInterval, void 0, { signal: AbortSignal.any([this.abortController.signal, this.currentLoopDelayInterrupt.signal]) });
|
|
131
|
+
try {
|
|
132
|
+
await node_timers_promises.setTimeout(MIN_POLLING_INTERVAL_MS, void 0, { signal: this.abortController.signal });
|
|
120
133
|
} catch (e) {
|
|
121
134
|
if (!(0, _milaboratories_pl_client.isTimeoutOrCancelError)(e)) throw new Error("Unexpected error", { cause: e });
|
|
122
135
|
if (this.abortController.signal.aborted) break;
|
|
123
|
-
}
|
|
124
|
-
|
|
136
|
+
}
|
|
137
|
+
if (!this.keepRunning || this.terminated) break;
|
|
138
|
+
if (this.scheduledOnNextState.length === 0) {
|
|
139
|
+
const remaining = Math.max(0, this.pollingInterval - MIN_POLLING_INTERVAL_MS);
|
|
140
|
+
if (remaining > 0) try {
|
|
141
|
+
this.currentLoopDelayInterrupt = new AbortController();
|
|
142
|
+
await node_timers_promises.setTimeout(remaining, void 0, { signal: AbortSignal.any([this.abortController.signal, this.currentLoopDelayInterrupt.signal]) });
|
|
143
|
+
} catch (e) {
|
|
144
|
+
if (!(0, _milaboratories_pl_client.isTimeoutOrCancelError)(e)) throw new Error("Unexpected error", { cause: e });
|
|
145
|
+
if (this.abortController.signal.aborted) break;
|
|
146
|
+
} finally {
|
|
147
|
+
this.currentLoopDelayInterrupt = void 0;
|
|
148
|
+
}
|
|
125
149
|
}
|
|
126
150
|
}
|
|
127
151
|
this.currentLoop = void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"synchronized_tree.cjs","names":["PlTreeState","PollingComputableHooks","PlTreeEntry","constructTreeLoadingRequest","loadTreeState","initialTreeLoadingStat","TreeStateUpdateError","tp"],"sources":["../src/synchronized_tree.ts"],"sourcesContent":["import { PollingComputableHooks } from \"@milaboratories/computable\";\nimport { PlTreeEntry } from \"./accessors\";\nimport type {\n FinalResourceDataPredicate,\n PlClient,\n SignedResourceId,\n TxOps,\n} from \"@milaboratories/pl-client\";\nimport { isTimeoutOrCancelError } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData } from \"./state\";\nimport { PlTreeState, TreeStateUpdateError } from \"./state\";\nimport type { PruningFunction, TreeLoadingStat } from \"./sync\";\nimport { constructTreeLoadingRequest, initialTreeLoadingStat, loadTreeState } from \"./sync\";\nimport * as tp from \"node:timers/promises\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\n\ntype StatLoggingMode = \"cumulative\" | \"per-request\";\n\nexport type SynchronizedTreeOps = {\n /** Override final predicate from the PlClient */\n finalPredicateOverride?: FinalResourceDataPredicate;\n\n /** Pruning function to limit set of fields through which tree will\n * traverse during state synchronization */\n pruning?: PruningFunction;\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\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 logStat?: StatLoggingMode;\n private readonly hooks: PollingComputableHooks;\n private readonly abortController = new AbortController();\n\n private constructor(\n private readonly pl: PlClient,\n private readonly root: SignedResourceId,\n ops: SynchronizedTreeOps,\n private readonly logger?: MiLogger,\n ) {\n const { finalPredicateOverride, pruning, pollingInterval, stopPollingDelay, logStat } = ops;\n this.pruning = pruning;\n this.pollingInterval = pollingInterval;\n this.finalPredicate = finalPredicateOverride ?? pl.finalPredicate;\n this.logStat = logStat;\n this.state = new PlTreeState(root, 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 /** @deprecated use \"entry\" instead */\n public accessor(rid: SignedResourceId = this.root): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return this.entry(rid);\n }\n\n public entry(rid: SignedResourceId = this.root): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeEntry({ treeProvider: () => this.state, hooks: this.hooks }, rid);\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 const request = constructTreeLoadingRequest(this.state, this.pruning);\n const data = await this.pl.withReadTx(\n \"ReadingTree\",\n async (tx) => {\n return await loadTreeState(tx, request, stats);\n },\n txOps,\n );\n this.state.updateFromResourceData(data, true);\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 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 // 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\n this.state = new PlTreeState(this.root, 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 if (this.scheduledOnNextState.length === 0) {\n try {\n this.currentLoopDelayInterrupt = new AbortController();\n await tp.setTimeout(this.pollingInterval, 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 the main abort controller fired, this is a permanent termination\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 // 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 public static async init(\n pl: PlClient,\n root: SignedResourceId,\n ops: SynchronizedTreeOps,\n logger?: MiLogger,\n ) {\n const tree = new SynchronizedTreeState(pl, root, ops, logger);\n\n const stat = ops.logStat ? initialTreeLoadingStat() : undefined;\n\n let ok = false;\n\n try {\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","constructTreeLoadingRequest","loadTreeState","initialTreeLoadingStat","TreeStateUpdateError","tp"],"sources":["../src/synchronized_tree.ts"],"sourcesContent":["import { PollingComputableHooks } from \"@milaboratories/computable\";\nimport { PlTreeEntry } from \"./accessors\";\nimport type {\n FinalResourceDataPredicate,\n PlClient,\n SignedResourceId,\n TxOps,\n} from \"@milaboratories/pl-client\";\nimport type { Filter } from \"@milaboratories/pl-client\";\nimport { 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\";\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;\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\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\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 private constructor(\n private readonly pl: PlClient,\n private readonly root: SignedResourceId,\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 this.state = new PlTreeState(root, 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 /** @deprecated use \"entry\" instead */\n public accessor(rid: SignedResourceId = this.root): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return this.entry(rid);\n }\n\n public entry(rid: SignedResourceId = this.root): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeEntry({ treeProvider: () => this.state, hooks: this.hooks }, rid);\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 const request = constructTreeLoadingRequest(this.state, {\n pruningFunction: this.pruning,\n fieldFilter: this.fieldFilter,\n traverseStopRules: this.traverseStopRules,\n });\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 /** 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 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 // 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\n this.state = new PlTreeState(this.root, 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 public static async init(\n pl: PlClient,\n root: SignedResourceId,\n ops: SynchronizedTreeOps,\n logger?: MiLogger,\n ) {\n const tree = new SynchronizedTreeState(pl, root, ops, logger);\n\n const stat = ops.logStat ? initialTreeLoadingStat() : undefined;\n\n let ok = false;\n\n try {\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,20 +1,21 @@
|
|
|
1
1
|
import { PlTreeEntry } from "./accessors.js";
|
|
2
2
|
import { ExtendedResourceData } from "./state.js";
|
|
3
|
-
import { PruningFunction } from "./sync.js";
|
|
4
|
-
import { FinalResourceDataPredicate, PlClient, SignedResourceId } from "@milaboratories/pl-client";
|
|
3
|
+
import { PruningFunction, TraversalMode } from "./sync.js";
|
|
4
|
+
import { Filter, FinalResourceDataPredicate, PlClient, SignedResourceId } from "@milaboratories/pl-client";
|
|
5
5
|
import { MiLogger } from "@milaboratories/ts-helpers";
|
|
6
6
|
|
|
7
7
|
//#region src/synchronized_tree.d.ts
|
|
8
8
|
type StatLoggingMode = "cumulative" | "per-request";
|
|
9
9
|
type SynchronizedTreeOps = {
|
|
10
|
-
/** Override final predicate from the PlClient */finalPredicateOverride?: FinalResourceDataPredicate;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/** Override final predicate from the PlClient */finalPredicateOverride?: FinalResourceDataPredicate; /** Pruning function for legacy fallback path. */
|
|
11
|
+
pruning?: PruningFunction; /** ResourceTree field filter for modern backend path. */
|
|
12
|
+
fieldFilter?: Filter; /** ResourceTree traversal stop rules for modern backend path. */
|
|
13
|
+
traverseStopRules?: Filter; /** Interval after last sync to sleep before the next one */
|
|
14
14
|
pollingInterval: number; /** For how long to continue polling after the last derived value access */
|
|
15
15
|
stopPollingDelay: number; /** If one of the values, tree will log stats of each polling request */
|
|
16
16
|
logStat?: StatLoggingMode; /** Timeout for initial tree loading. If not specified, will use default for RO tx from pl-client. */
|
|
17
|
-
initialTreeLoadingTimeout?: number;
|
|
17
|
+
initialTreeLoadingTimeout?: number; /** Controls which tree-loading path to use. Default `"auto"`. */
|
|
18
|
+
traversalMode?: TraversalMode;
|
|
18
19
|
};
|
|
19
20
|
declare class SynchronizedTreeState {
|
|
20
21
|
private readonly pl;
|
|
@@ -24,6 +25,9 @@ declare class SynchronizedTreeState {
|
|
|
24
25
|
private state;
|
|
25
26
|
private readonly pollingInterval;
|
|
26
27
|
private readonly pruning?;
|
|
28
|
+
private readonly fieldFilter?;
|
|
29
|
+
private readonly traverseStopRules?;
|
|
30
|
+
private readonly traversalMode;
|
|
27
31
|
private readonly logStat?;
|
|
28
32
|
private readonly hooks;
|
|
29
33
|
private readonly abortController;
|
|
@@ -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":";;;;;;;KAsBK,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;AAAA,cAQL,qBAAA;EAAA,iBAaQ,EAAA;EAAA,iBACA,IAAA;EAAA,iBAEA,MAAA;EAAA,iBAfF,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;EAAA,QAEV,WAAA,CAAA;;EAiCA,QAAA,CAAS,GAAA,GAAK,gBAAA,GAA+B,WAAA;EAK7C,KAAA,CAAM,GAAA,GAAK,gBAAA,GAA+B,WAAA;EAA/B;;EAOL,YAAA,CAAA,GAAgB,OAAA;EAAA,QAKrB,yBAAA;EAAA,QACA,oBAAA;EAgMiC;EAAA,QA7LjC,mBAAA;EAoMA;EAAA,QAxLA,aAAA;EA0LG;EAAA,QAnLH,YAAA;EAmLW;EAAA,QA9KX,WAAA;EA8KW;EAAA,QA5KX,WAAA;EA9EW;EAAA,QAiFL,OAAA;EA9FG;EAAA,QAuHT,UAAA;EAAA,QAEM,QAAA;EAtHG;;;;EAuOV,SAAA,CAAA,GAAa,oBAAA;EAlOH;;;;EA0OJ,SAAA,CAAA,GAAa,OAAA;EAtMV;EAkNH,wBAAA,CAAA,GAA4B,OAAA;EAAA,OAKrB,IAAA,CAClB,EAAA,EAAI,QAAA,EACJ,IAAA,EAAM,gBAAA,EACN,GAAA,EAAK,mBAAA,EACL,MAAA,GAAS,QAAA,GAAQ,OAAA,CAAA,qBAAA;AAAA"}
|
|
@@ -5,11 +5,18 @@ import { isTimeoutOrCancelError } 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
|
|
8
|
+
/** Hard floor between consecutive tree-refresh calls.
|
|
9
|
+
* Applies even when {@link scheduleOnNextState} has woken the loop early,
|
|
10
|
+
* preventing tight polling loops during rapid state transitions. */
|
|
11
|
+
const MIN_POLLING_INTERVAL_MS = 100;
|
|
8
12
|
var SynchronizedTreeState = class SynchronizedTreeState {
|
|
9
13
|
finalPredicate;
|
|
10
14
|
state;
|
|
11
15
|
pollingInterval;
|
|
12
16
|
pruning;
|
|
17
|
+
fieldFilter;
|
|
18
|
+
traverseStopRules;
|
|
19
|
+
traversalMode;
|
|
13
20
|
logStat;
|
|
14
21
|
hooks;
|
|
15
22
|
abortController = new AbortController();
|
|
@@ -17,8 +24,11 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
17
24
|
this.pl = pl;
|
|
18
25
|
this.root = root;
|
|
19
26
|
this.logger = logger;
|
|
20
|
-
const { finalPredicateOverride, pruning, pollingInterval, stopPollingDelay, logStat } = ops;
|
|
27
|
+
const { finalPredicateOverride, pruning, fieldFilter, traverseStopRules, traversalMode, pollingInterval, stopPollingDelay, logStat } = ops;
|
|
21
28
|
this.pruning = pruning;
|
|
29
|
+
this.fieldFilter = fieldFilter;
|
|
30
|
+
this.traverseStopRules = traverseStopRules;
|
|
31
|
+
this.traversalMode = traversalMode ?? "auto";
|
|
22
32
|
this.pollingInterval = pollingInterval;
|
|
23
33
|
this.finalPredicate = finalPredicateOverride ?? pl.finalPredicate;
|
|
24
34
|
this.logStat = logStat;
|
|
@@ -76,9 +86,13 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
76
86
|
/** Executed from the main loop, and initialization procedure. */
|
|
77
87
|
async refresh(stats, txOps) {
|
|
78
88
|
if (this.terminated) throw new Error("tree synchronization is terminated");
|
|
79
|
-
const request = constructTreeLoadingRequest(this.state,
|
|
89
|
+
const request = constructTreeLoadingRequest(this.state, {
|
|
90
|
+
pruningFunction: this.pruning,
|
|
91
|
+
fieldFilter: this.fieldFilter,
|
|
92
|
+
traverseStopRules: this.traverseStopRules
|
|
93
|
+
});
|
|
80
94
|
const data = await this.pl.withReadTx("ReadingTree", async (tx) => {
|
|
81
|
-
return await loadTreeState(tx, request, stats);
|
|
95
|
+
return await loadTreeState(tx, request, stats, this.pl.serverInfo.capabilities ?? [], this.traversalMode, this.logger);
|
|
82
96
|
}, txOps);
|
|
83
97
|
this.state.updateFromResourceData(data, true);
|
|
84
98
|
}
|
|
@@ -112,14 +126,24 @@ var SynchronizedTreeState = class SynchronizedTreeState {
|
|
|
112
126
|
} else this.logger?.warn(e);
|
|
113
127
|
}
|
|
114
128
|
if (!this.keepRunning || this.terminated) break;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
await tp.setTimeout(this.pollingInterval, void 0, { signal: AbortSignal.any([this.abortController.signal, this.currentLoopDelayInterrupt.signal]) });
|
|
129
|
+
try {
|
|
130
|
+
await tp.setTimeout(MIN_POLLING_INTERVAL_MS, void 0, { signal: this.abortController.signal });
|
|
118
131
|
} catch (e) {
|
|
119
132
|
if (!isTimeoutOrCancelError(e)) throw new Error("Unexpected error", { cause: e });
|
|
120
133
|
if (this.abortController.signal.aborted) break;
|
|
121
|
-
}
|
|
122
|
-
|
|
134
|
+
}
|
|
135
|
+
if (!this.keepRunning || this.terminated) break;
|
|
136
|
+
if (this.scheduledOnNextState.length === 0) {
|
|
137
|
+
const remaining = Math.max(0, this.pollingInterval - MIN_POLLING_INTERVAL_MS);
|
|
138
|
+
if (remaining > 0) try {
|
|
139
|
+
this.currentLoopDelayInterrupt = new AbortController();
|
|
140
|
+
await tp.setTimeout(remaining, void 0, { signal: AbortSignal.any([this.abortController.signal, this.currentLoopDelayInterrupt.signal]) });
|
|
141
|
+
} catch (e) {
|
|
142
|
+
if (!isTimeoutOrCancelError(e)) throw new Error("Unexpected error", { cause: e });
|
|
143
|
+
if (this.abortController.signal.aborted) break;
|
|
144
|
+
} finally {
|
|
145
|
+
this.currentLoopDelayInterrupt = void 0;
|
|
146
|
+
}
|
|
123
147
|
}
|
|
124
148
|
}
|
|
125
149
|
this.currentLoop = void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"synchronized_tree.js","names":[],"sources":["../src/synchronized_tree.ts"],"sourcesContent":["import { PollingComputableHooks } from \"@milaboratories/computable\";\nimport { PlTreeEntry } from \"./accessors\";\nimport type {\n FinalResourceDataPredicate,\n PlClient,\n SignedResourceId,\n TxOps,\n} from \"@milaboratories/pl-client\";\nimport { isTimeoutOrCancelError } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData } from \"./state\";\nimport { PlTreeState, TreeStateUpdateError } from \"./state\";\nimport type { PruningFunction, TreeLoadingStat } from \"./sync\";\nimport { constructTreeLoadingRequest, initialTreeLoadingStat, loadTreeState } from \"./sync\";\nimport * as tp from \"node:timers/promises\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\n\ntype StatLoggingMode = \"cumulative\" | \"per-request\";\n\nexport type SynchronizedTreeOps = {\n /** Override final predicate from the PlClient */\n finalPredicateOverride?: FinalResourceDataPredicate;\n\n /** Pruning function to limit set of fields through which tree will\n * traverse during state synchronization */\n pruning?: PruningFunction;\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\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 logStat?: StatLoggingMode;\n private readonly hooks: PollingComputableHooks;\n private readonly abortController = new AbortController();\n\n private constructor(\n private readonly pl: PlClient,\n private readonly root: SignedResourceId,\n ops: SynchronizedTreeOps,\n private readonly logger?: MiLogger,\n ) {\n const { finalPredicateOverride, pruning, pollingInterval, stopPollingDelay, logStat } = ops;\n this.pruning = pruning;\n this.pollingInterval = pollingInterval;\n this.finalPredicate = finalPredicateOverride ?? pl.finalPredicate;\n this.logStat = logStat;\n this.state = new PlTreeState(root, 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 /** @deprecated use \"entry\" instead */\n public accessor(rid: SignedResourceId = this.root): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return this.entry(rid);\n }\n\n public entry(rid: SignedResourceId = this.root): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeEntry({ treeProvider: () => this.state, hooks: this.hooks }, rid);\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 const request = constructTreeLoadingRequest(this.state, this.pruning);\n const data = await this.pl.withReadTx(\n \"ReadingTree\",\n async (tx) => {\n return await loadTreeState(tx, request, stats);\n },\n txOps,\n );\n this.state.updateFromResourceData(data, true);\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 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 // 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\n this.state = new PlTreeState(this.root, 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 if (this.scheduledOnNextState.length === 0) {\n try {\n this.currentLoopDelayInterrupt = new AbortController();\n await tp.setTimeout(this.pollingInterval, 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 the main abort controller fired, this is a permanent termination\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 // 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 public static async init(\n pl: PlClient,\n root: SignedResourceId,\n ops: SynchronizedTreeOps,\n logger?: MiLogger,\n ) {\n const tree = new SynchronizedTreeState(pl, root, ops, logger);\n\n const stat = ops.logStat ? initialTreeLoadingStat() : undefined;\n\n let ok = false;\n\n try {\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 } from \"./accessors\";\nimport type {\n FinalResourceDataPredicate,\n PlClient,\n SignedResourceId,\n TxOps,\n} from \"@milaboratories/pl-client\";\nimport type { Filter } from \"@milaboratories/pl-client\";\nimport { 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\";\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;\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\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\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 private constructor(\n private readonly pl: PlClient,\n private readonly root: SignedResourceId,\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 this.state = new PlTreeState(root, 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 /** @deprecated use \"entry\" instead */\n public accessor(rid: SignedResourceId = this.root): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return this.entry(rid);\n }\n\n public entry(rid: SignedResourceId = this.root): PlTreeEntry {\n if (this.terminated) throw new Error(\"tree synchronization is terminated\");\n return new PlTreeEntry({ treeProvider: () => this.state, hooks: this.hooks }, rid);\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 const request = constructTreeLoadingRequest(this.state, {\n pruningFunction: this.pruning,\n fieldFilter: this.fieldFilter,\n traverseStopRules: this.traverseStopRules,\n });\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 /** 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 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 // 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\n this.state = new PlTreeState(this.root, 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 public static async init(\n pl: PlClient,\n root: SignedResourceId,\n ops: SynchronizedTreeOps,\n logger?: MiLogger,\n ) {\n const tree = new SynchronizedTreeState(pl, root, ops, logger);\n\n const stat = ops.logStat ? initialTreeLoadingStat() : undefined;\n\n let ok = false;\n\n try {\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.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "Reactive pl tree state",
|
|
5
5
|
"files": [
|
|
6
6
|
"./dist/**/*",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"denque": "^2.1.0",
|
|
22
22
|
"utility-types": "^3.11.0",
|
|
23
23
|
"zod": "~3.25.76",
|
|
24
|
-
"@milaboratories/
|
|
25
|
-
"@milaboratories/pl-
|
|
26
|
-
"@milaboratories/
|
|
27
|
-
"@milaboratories/
|
|
24
|
+
"@milaboratories/computable": "2.9.4",
|
|
25
|
+
"@milaboratories/pl-errors": "1.4.8",
|
|
26
|
+
"@milaboratories/pl-client": "3.6.0",
|
|
27
|
+
"@milaboratories/ts-helpers": "1.8.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/node": "~24.5.2",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"typescript": "~5.9.3",
|
|
33
33
|
"vitest": "^4.1.3",
|
|
34
34
|
"@milaboratories/build-configs": "2.0.0",
|
|
35
|
-
"@milaboratories/ts-
|
|
36
|
-
"@milaboratories/ts-
|
|
35
|
+
"@milaboratories/ts-builder": "1.5.0",
|
|
36
|
+
"@milaboratories/ts-configs": "1.2.3"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=22.19.0"
|