From 6891d03af917e962d10a40aa7f92c4b39c1404a9 Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Fri, 9 Nov 2018 15:36:52 +0100 Subject: [PATCH] mol-state: wip immutable tree --- src/mol-state/immutable-tree.1.ts | 246 ++++++++++++++++-------------- src/mol-state/state.ts | 6 +- src/mol-state/transform.ts | 23 +-- src/mol-state/transformer.ts | 4 +- src/mol-state/tree.ts | 2 +- 5 files changed, 148 insertions(+), 133 deletions(-) diff --git a/src/mol-state/immutable-tree.1.ts b/src/mol-state/immutable-tree.1.ts index ee026fdb3..88c807a9d 100644 --- a/src/mol-state/immutable-tree.1.ts +++ b/src/mol-state/immutable-tree.1.ts @@ -15,36 +15,34 @@ export { ImmutableTree, TransientTree } */ interface ImmutableTree { readonly version: number, + readonly root: Transform, readonly nodes: ImmutableTree.Nodes, - readonly root: ImmutableTree.Node, - get(ref: ImmutableTree.Ref): Transform | undefined, - //getChildren(ref: ImmutableTree.Ref): OrderedSet<ImmutableTree.Node> + readonly children: ImmutableTree.Children, asTransient(): TransientTree } namespace ImmutableTree { - export type Ref = Transform.Ref - export interface Node extends Readonly<TransientTree.Node> { } - export interface Nodes extends ImmutableMap<ImmutableTree.Ref, Node> { } + type Ref = Transform.Ref - class Impl implements ImmutableTree { - readonly version: number; - readonly nodes: ImmutableTree.Nodes; + export interface ChildSet { + readonly size: number, + readonly values: OrderedSet<Ref>['values'], + has(ref: Ref): boolean, + readonly forEach: OrderedSet<Ref>['forEach'] + } - get root() { return this.nodes.get(Transform.RootRef)! } + export type Node = Transform + export type Nodes = ImmutableMap<Ref, Transform> + export type Children = ImmutableMap<Ref, ChildSet> - get(ref: Ref) { - const n = this.nodes.get(ref); - return n ? n.value : void 0; - } + class Impl implements ImmutableTree { + get root() { return this.nodes.get(Transform.RootRef)! } asTransient(): TransientTree { return new TransientTree(this); } - constructor(nodes: ImmutableTree.Nodes, version: number) { - this.nodes = nodes; - this.version = version; + constructor(public nodes: ImmutableTree.Nodes, public children: Children, public version: number) { } } @@ -52,39 +50,40 @@ namespace ImmutableTree { * Create an instance of an immutable tree. */ export function create<T>(root: Transform): ImmutableTree { - const r: Node = { value: root, version: 0, parent: root.ref, children: OrderedSet() }; - return new Impl(ImmutableMap([[root.ref, r]]), 0); + return new Impl(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]), 0); } - export function fromNodes<T>(nodes: Nodes, version: number): ImmutableTree { - return new Impl(nodes, version); + export function construct<T>(nodes: Nodes, children: Children, version: number): ImmutableTree { + return new Impl(nodes, children, version); } - type VisitorCtx = { nodes: Nodes, state: any, f: (node: Node, nodes: Nodes, state: any) => boolean | undefined | void }; + type VisitorCtx = { tree: ImmutableTree, state: any, f: (node: Node, tree: ImmutableTree, state: any) => boolean | undefined | void }; - function _postOrderFunc(this: VisitorCtx, c: ImmutableTree.Ref | undefined) { _doPostOrder(this, this.nodes.get(c!)!); } + function _postOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPostOrder(this, this.tree.nodes.get(c!)); } function _doPostOrder(ctx: VisitorCtx, root: Node) { - if (root.children.size) { - root.children.forEach(_postOrderFunc, ctx); + const children = ctx.tree.children.get(root.ref); + if (checkSetRef && children.size) { + children.forEach(_postOrderFunc, ctx); } - ctx.f(root, ctx.nodes, ctx.state); + ctx.f(root, ctx.tree, ctx.state); } /** * Visit all nodes in a subtree in "post order", meaning leafs get visited first. */ - export function doPostOrder<S>(tree: ImmutableTree, root: Node, state: S, f: (node: Node, nodes: Nodes, state: S) => boolean | undefined | void): S { - const ctx: VisitorCtx = { nodes: tree.nodes, state, f }; + export function doPostOrder<S>(tree: ImmutableTree, root: Node, state: S, f: (node: Node, tree: ImmutableTree, state: S) => boolean | undefined | void): S { + const ctx: VisitorCtx = { tree, state, f }; _doPostOrder(ctx, root); return ctx.state; } - function _preOrderFunc(this: VisitorCtx, c: ImmutableTree.Ref | undefined) { _doPreOrder(this, this.nodes.get(c!)!); } + function _preOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPreOrder(this, this.tree.nodes.get(c!)); } function _doPreOrder(ctx: VisitorCtx, root: Node) { - const ret = ctx.f(root, ctx.nodes, ctx.state); + const ret = ctx.f(root, ctx.tree, ctx.state); if (typeof ret === 'boolean' && !ret) return; - if (root.children.size) { - root.children.forEach(_preOrderFunc, ctx); + const children = ctx.tree.children.get(root.ref); + if (checkSetRef && children.size) { + children.forEach(_preOrderFunc, ctx); } } @@ -92,13 +91,13 @@ namespace ImmutableTree { * Visit all nodes in a subtree in "pre order", meaning leafs get visited last. * If the visitor function returns false, the visiting for that branch is interrupted. */ - export function doPreOrder<S>(tree: ImmutableTree, root: Node, state: S, f: (node: Node, nodes: Nodes, state: S) => boolean | undefined | void): S { - const ctx: VisitorCtx = { nodes: tree.nodes, state, f }; + export function doPreOrder<S>(tree: ImmutableTree, root: Node, state: S, f: (node: Node, tree: ImmutableTree, state: S) => boolean | undefined | void): S { + const ctx: VisitorCtx = { tree, state, f }; _doPreOrder(ctx, root); return ctx.state; } - function _subtree(n: Node, nodes: Nodes, subtree: Node[]) { subtree.push(n); } + function _subtree(n: Node, _: any, subtree: Node[]) { subtree.push(n); } /** * Get all nodes in a subtree, leafs come first. */ @@ -108,19 +107,19 @@ namespace ImmutableTree { function _visitChildToJson(this: Ref[], ref: Ref) { this.push(ref); } - interface ToJsonCtx { nodes: [Transform.Serialized, any, any[]][] } + interface ToJsonCtx { tree: ImmutableTree, nodes: [Transform.Serialized, any[]][] } function _visitNodeToJson(this: ToJsonCtx, node: Node) { const children: Ref[] = []; - node.children.forEach(_visitChildToJson as any, children); - this.nodes.push([Transform.toJSON(node.value), node.parent, children]); + this.tree.children.get(node.ref).forEach(_visitChildToJson as any, children); + this.nodes.push([Transform.toJSON(node), children]); } export interface Serialized { - nodes: [any /** value */, number /** parent index */, number[] /** children indices */][] + nodes: [any /** value */, number[] /** children indices */][] } export function toJSON<T>(tree: ImmutableTree): Serialized { - const ctx: ToJsonCtx = { nodes: [] }; + const ctx: ToJsonCtx = { tree, nodes: [] }; tree.nodes.forEach(_visitNodeToJson as any, ctx); @@ -129,8 +128,7 @@ namespace ImmutableTree { for (const n of ctx.nodes) map.set(n[0].ref, i++); for (const n of ctx.nodes) { - n[1] = map.get(n[1]); - const children = n[2]; + const children = n[1]; for (i = 0; i < children.length; i++) { children[i] = map.get(children[i]); } @@ -141,102 +139,131 @@ namespace ImmutableTree { } export function fromJSON<T>(data: Serialized): ImmutableTree { - const nodes = ImmutableMap<ImmutableTree.Ref, Node>().asMutable(); + const nodes = ImmutableMap<Ref, Node>().asMutable(); + const children = ImmutableMap<Ref, OrderedSet<Ref>>().asMutable(); const values = data.nodes.map(n => Transform.fromJSON(n[0])); let i = 0; for (const value of values) { const node = data.nodes[i++]; const ref = value.ref; - nodes.set(ref, { - value, - version: 0, - parent: values[node[1]].ref, - children: OrderedSet(node[2].map(c => values[c].ref)) - }); + nodes.set(ref, value); + children.set(ref, OrderedSet(node[1].map(c => values[c].ref))); } - return new Impl(nodes.asImmutable(), 0); + return new Impl(nodes.asImmutable(), children.asImmutable(), 0); } } class TransientTree implements ImmutableTree { nodes = this.tree.nodes.asMutable(); + children = this.tree.children.asMutable(); + version: number = this.tree.version + 1; - private mutations: Map<Transform.Ref, TransientTree.Node> = new Map(); - get root() { return this.nodes.get(Transform.RootRef)! } + private mutations: Map<Transform.Ref, OrderedSet<Transform.Ref>> = new Map(); - get(ref: Transform.Ref) { - const n = this.nodes.get(ref); - return n ? n.value : void 0; - } + get root() { return this.nodes.get(Transform.RootRef)! } asTransient() { return this.asImmutable().asTransient(); } - mutate(ref: ImmutableTree.Ref): TransientTree.Node { - return mutateNode(this.nodes, this.mutations, ref); + private addChild(parent: Transform.Ref, child: Transform.Ref) { + if (this.mutations.has(parent)) { + this.mutations.get(parent)!.add(child); + } else { + const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable(); + set.add(child); + this.children.set(parent, set); + this.mutations.set(parent, set); + } } - add(parentRef: ImmutableTree.Ref, value: Transform) { - const ref = value.ref; - ensureNotPresent(this.nodes, ref); - const parent = this.mutate(parentRef); - const node: TransientTree.Node = { version: 0, value, parent: parentRef, children: OrderedSet<string>().asMutable() }; - this.mutations.set(ref, node); - parent.children.add(ref); - this.nodes.set(ref, node); - return node; + private removeChild(parent: Transform.Ref, child: Transform.Ref) { + if (this.mutations.has(parent)) { + this.mutations.get(parent)!.remove(child); + } else { + const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable(); + set.remove(child); + this.children.set(parent, set); + this.mutations.set(parent, set); + } } - setValue(ref: ImmutableTree.Ref, value: Transform): TransientTree.Node { - checkSetRef(ref, value.ref); - const node = this.mutate(ref); - node.value = value; - return node; + private clearRoot() { + const parent = Transform.RootRef; + if (this.children.get(parent).size === 0) return; + const set = OrderedSet<Transform.Ref>(); + this.children.set(parent, set); + this.mutations.set(parent, set); } - remove(ref: ImmutableTree.Ref): TransientTree.Node[] { - if (ref === Transform.RootRef) { - return this.removeChildren(ref); + add(transform: Transform) { + const ref = transform.ref; + + const children = this.children.get(transform.parent); + if (!children) parentNotPresent(transform.parent); + + if (!children.has(transform.ref)) { + this.addChild(transform.parent, transform.ref); + } + + if (!this.children.has(transform.ref)) { + this.children.set(transform.ref, OrderedSet()); } - const { nodes, mutations } = this; + this.nodes.set(ref, transform); + return transform; + } + + set(transform: Transform) { + ensurePresent(this.nodes, transform.ref); + this.nodes.set(transform.ref, transform); + } + + remove(ref: Transform.Ref): Transform[] { + const { nodes, mutations, children } = this; const node = nodes.get(ref); if (!node) return []; - this.mutate(node.parent).children.delete(ref); const st = ImmutableTree.subtreePostOrder(this, node); - for (const n of st) { - nodes.delete(n.value.ref); - mutations.delete(n.value.ref); + if (ref === Transform.RootRef) { + st.pop(); + this.clearRoot(); + } else { + this.removeChild(node.parent, node.ref); } - return st; - } - - removeChildren(ref: ImmutableTree.Ref): TransientTree.Node[] { - const { nodes, mutations } = this; - let node = nodes.get(ref); - if (!node || !node.children.size) return []; - node = this.mutate(ref); - const st = ImmutableTree.subtreePostOrder(this, node); - // remove the last node which is the parent - st.pop(); - node.children.clear(); for (const n of st) { - nodes.delete(n.value.ref); - mutations.delete(n.value.ref); + nodes.delete(n.ref); + children.delete(n.ref); + mutations.delete(n.ref); } + return st; } + // removeChildren(ref: ImmutableTree.Ref): TransientTree.Node[] { + // const { nodes, mutations } = this; + // let node = nodes.get(ref); + // if (!node || !node.children.size) return []; + // node = this.mutate(ref); + // const st = ImmutableTree.subtreePostOrder(this, node); + // // remove the last node which is the parent + // st.pop(); + // node.children.clear(); + // for (const n of st) { + // nodes.delete(n.value.ref); + // mutations.delete(n.value.ref); + // } + // return st; + // } + asImmutable() { if (this.mutations.size === 0) return this.tree; - this.mutations.forEach(m => (m as TransientTree.Node).children = m.children.asImmutable()); - return ImmutableTree.fromNodes(this.nodes.asMutable(), this.version); + this.mutations.forEach(fixChildMutations, this.children); + return ImmutableTree.construct(this.nodes.asImmutable(), this.children.asImmutable(), this.version); } constructor(private tree: ImmutableTree) { @@ -244,37 +271,20 @@ class TransientTree implements ImmutableTree { } } -namespace TransientTree { - export interface Node { value: Transform, version: number, parent: ImmutableTree.Ref, children: OrderedSet<ImmutableTree.Ref> } -} - +function fixChildMutations(this: ImmutableTree.Children, m: OrderedSet<Transform.Ref>, k: Transform.Ref) { this.set(k, m.asImmutable()); } -function checkSetRef(oldRef: ImmutableTree.Ref, newRef: ImmutableTree.Ref) { +function checkSetRef(oldRef: Transform.Ref, newRef: Transform.Ref) { if (oldRef !== newRef) { throw new Error(`Cannot setValue of node '${oldRef}' because the new value has a different ref '${newRef}'.`); } } -function ensureNotPresent(nodes: ImmutableTree.Nodes, ref: ImmutableTree.Ref) { - if (nodes.has(ref)) { - throw new Error(`Cannot add node '${ref}' because a different node with this ref already present in the tree.`); - } +function parentNotPresent(ref: Transform.Ref) { + throw new Error(`Parent '${ref}' must be present in the tree.`); } -function ensurePresent(nodes: ImmutableTree.Nodes, ref: ImmutableTree.Ref) { +function ensurePresent(nodes: ImmutableTree.Nodes, ref: Transform.Ref) { if (!nodes.has(ref)) { throw new Error(`Node '${ref}' is not present in the tree.`); } -} - -function mutateNode(nodes: ImmutableTree.Nodes, mutations: Map<ImmutableTree.Ref, TransientTree.Node>, ref: ImmutableTree.Ref): TransientTree.Node { - ensurePresent(nodes, ref); - if (mutations.has(ref)) { - return mutations.get(ref)!; - } - const node = nodes.get(ref)!; - const newNode: TransientTree.Node = { value: node.value, version: node.version + 1, parent: node.parent, children: node.children.asMutable() }; - mutations.set(ref, newNode); - nodes.set(ref, newNode); - return newNode; } \ No newline at end of file diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index f8f779a8e..2c42a29e7 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -268,7 +268,7 @@ namespace State { obj, status: 'ok', version: transform.version, - state: { ...ctx.stateCtx.defaultCellState, ...transform.defaultProps } + state: { ...ctx.stateCtx.defaultCellState, ...transform.cellState } }); return { action: 'created', obj }; } else { @@ -288,13 +288,13 @@ namespace State { obj, status: 'ok', version: transform.version, - state: { ...ctx.stateCtx.defaultCellState, ...current.state, ...transform.defaultProps } + state: { ...ctx.stateCtx.defaultCellState, ...current.state, ...transform.cellState } }); return { action: 'replaced', oldObj: current.obj!, newObj: obj }; } case Transformer.UpdateResult.Updated: current.version = transform.version; - current.state = { ...ctx.stateCtx.defaultCellState, ...current.state, ...transform.defaultProps }; + current.state = { ...ctx.stateCtx.defaultCellState, ...current.state, ...transform.cellState }; return { action: 'updated', obj: current.obj }; default: // TODO check if props need to be updated diff --git a/src/mol-state/transform.ts b/src/mol-state/transform.ts index 99d15b81d..95ed6cd8c 100644 --- a/src/mol-state/transform.ts +++ b/src/mol-state/transform.ts @@ -4,16 +4,17 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { StateObject } from './object'; +import { StateObject, StateObjectCell } from './object'; import { Transformer } from './transformer'; import { UUID } from 'mol-util'; export interface Transform<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { + readonly parent: Transform.Ref, readonly transformer: Transformer<A, B, P>, readonly params: P, readonly ref: Transform.Ref, readonly version: string, - readonly defaultProps?: unknown + readonly cellState?: Partial<StateObjectCell.State> } export namespace Transform { @@ -21,16 +22,17 @@ export namespace Transform { export const RootRef = '-=root=-' as Ref; - export interface Options { ref?: Ref, defaultProps?: unknown } + export interface Options { ref?: Ref, cellState?: Partial<StateObjectCell.State> } - export function create<A extends StateObject, B extends StateObject, P>(transformer: Transformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> { + export function create<A extends StateObject, B extends StateObject, P>(parent: Ref, transformer: Transformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> { const ref = options && options.ref ? options.ref : UUID.create() as string as Ref; return { + parent, transformer, params: params || {} as any, ref, version: UUID.create(), - defaultProps: options && options.defaultProps + cellState: options && options.cellState } } @@ -39,15 +41,16 @@ export namespace Transform { } export function createRoot(ref: Ref): Transform { - return create(Transformer.ROOT, {}, { ref }); + return create(RootRef, Transformer.ROOT, {}, { ref }); } export interface Serialized { + parent: string, transformer: string, params: any, ref: string, version: string, - defaultProps?: unknown + cellState?: Partial<StateObjectCell.State> } function _id(x: any) { return x; } @@ -56,11 +59,12 @@ export namespace Transform { ? t.transformer.definition.customSerialization.toJSON : _id; return { + parent: t.parent, transformer: t.transformer.id, params: pToJson(t.params), ref: t.ref, version: t.version, - defaultProps: t.defaultProps + cellState: t.cellState }; } @@ -70,11 +74,12 @@ export namespace Transform { ? transformer.definition.customSerialization.toJSON : _id; return { + parent: t.parent as Ref, transformer, params: pFromJson(t.params), ref: t.ref as Ref, version: t.version, - defaultProps: t.defaultProps + cellState: t.cellState }; } } \ No newline at end of file diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index 914dda111..375c9a950 100644 --- a/src/mol-state/transformer.ts +++ b/src/mol-state/transformer.ts @@ -10,7 +10,7 @@ import { Transform } from './transform'; import { ParamDefinition as PD } from 'mol-util/param-definition'; export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { - apply(params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>, + apply(parent: Transform.Ref, params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>, readonly namespace: string, readonly id: Transformer.Id, readonly definition: Transformer.Definition<A, B, P> @@ -114,7 +114,7 @@ export namespace Transformer { } const t: Transformer<A, B, P> = { - apply(params, props) { return Transform.create<A, B, P>(t as any, params, props); }, + apply(parent, params, props) { return Transform.create<A, B, P>(parent, t as any, params, props); }, namespace, id, definition diff --git a/src/mol-state/tree.ts b/src/mol-state/tree.ts index d26bf4902..5c71179b0 100644 --- a/src/mol-state/tree.ts +++ b/src/mol-state/tree.ts @@ -64,7 +64,7 @@ namespace StateTree { export class To<A extends StateObject> implements Builder { apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, props?: Partial<Transform.Options>): To<Transformer.To<T>> { - const t = tr.apply(params, props); + const t = tr.apply(this.ref, params, props); this.state.tree.add(this.ref, t); return new To(this.state, t.ref, this.root); } -- GitLab