mobx-keystone
Version:
A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more
183 lines (155 loc) • 4.44 kB
text/typescript
import { computed, IComputedValue } from "mobx"
import { assertTweakedObject } from "../tweaker/core"
import { getObjectChildren } from "./coreObjectChildren"
/**
* Mode for the `walkTree` method.
*/
export enum WalkTreeMode {
/**
* The walk will be done parent (roots) first, then children.
*/
ParentFirst = "parentFirst",
/**
* The walk will be done children (leafs) first, then parents.
*/
ChildrenFirst = "childrenFirst",
}
/**
* Walks a tree, running the predicate function for each node.
* If the predicate function returns something other than undefined,
* then the walk will be stopped and the function will return the returned value.
*
* @template T Returned object type, defaults to void.
* @param root Subtree root object.
* @param visit Function that will be run for each node of the tree.
* @param mode Mode to walk the tree, as defined in `WalkTreeMode`.
* @returns
*/
export function walkTree<T = void>(
root: object,
visit: (node: object) => T | undefined,
mode: WalkTreeMode
): T | undefined {
assertTweakedObject(root, "root")
if (mode === WalkTreeMode.ParentFirst) {
return walkTreeParentFirst(root, visit)
} else {
return walkTreeChildrenFirst(root, visit)
}
}
function walkTreeParentFirst<T = void>(
root: object,
visit: (node: object) => T | undefined
): T | undefined {
const stack: object[] = [root]
while (stack.length > 0) {
const node = stack.pop()!
const ret = visit(node)
if (ret !== undefined) {
return ret
}
const children = getObjectChildren(node)
stack.length += children.size
let i = stack.length - 1
const childrenIter = children.values()
let ch = childrenIter.next()
while (!ch.done) {
stack[i--] = ch.value
ch = childrenIter.next()
}
}
return undefined
}
function walkTreeChildrenFirst<T = void>(
root: object,
visit: (node: object) => T | undefined
): T | undefined {
const childrenIter = getObjectChildren(root).values()
let ch = childrenIter.next()
while (!ch.done) {
const ret = walkTreeChildrenFirst(ch.value, visit)
if (ret !== undefined) {
return ret
}
ch = childrenIter.next()
}
const ret = visit(root)
if (ret !== undefined) {
return ret
}
return undefined
}
/**
* @internal
*/
export interface ComputedWalkTreeAggregate<R> {
walk(target: object): Map<R, object> | undefined
}
function getComputedTreeResult<R>(
computedFns: WeakMap<object, IComputedValue<Map<R, object> | undefined>>,
visit: (node: object) => R | undefined,
tree: object
): Map<R, object> | undefined {
let cmpted = computedFns.get(tree)
if (!cmpted) {
cmpted = computed(() => {
return walkTreeAggregate(tree, visit, (ch) => getComputedTreeResult(computedFns, visit, ch))
})
computedFns.set(tree, cmpted)
}
return cmpted.get()
}
/**
* @internal
*/
export function computedWalkTreeAggregate<R>(
visit: (node: object) => R | undefined
): ComputedWalkTreeAggregate<R> {
const computedFns = new WeakMap<object, IComputedValue<Map<R, object> | undefined>>()
return {
walk: (n) => getComputedTreeResult(computedFns, visit, n),
}
}
function walkTreeAggregate<R>(
target: object,
visit: (node: object) => R | undefined,
recurse: (node: object) => Map<R, object> | undefined
): Map<R, object> | undefined {
let map: Map<R, object> | undefined
const rootVal = visit(target)
const childrenMap = getObjectChildren(target)
const childrenIter = childrenMap.values()
let ch = childrenIter.next()
// small optimization, if there is only one child and this
// object provides no value we can just reuse the child ones
if (rootVal === undefined && childrenMap.size === 1) {
return recurse(ch.value!)
}
while (!ch.done) {
const childMap = recurse(ch.value)
if (childMap) {
if (!map) {
map = new Map()
}
// add child map keys/values to own map
const mapIter = childMap.keys()
let mapCur = mapIter.next()
while (!mapCur.done) {
const key = mapCur.value
const val = childMap.get(key)!
map.set(key, val)
mapCur = mapIter.next()
}
}
ch = childrenIter.next()
}
// add it at the end so parent resolutions have higher
// priority than child ones
if (rootVal !== undefined) {
if (!map) {
map = new Map()
}
map.set(rootVal, target)
}
return map
}