From b3b43778662ee96565f5f40a6c75a24c7336eb07 Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Fri, 9 Nov 2018 18:00:32 +0100
Subject: [PATCH] mol-state: wip

---
 src/mol-plugin/behavior/built-in/data.ts |   3 +-
 src/mol-plugin/context.ts                |   6 +-
 src/mol-plugin/state.ts                  |   4 +-
 src/mol-state/context.ts                 |   4 +-
 src/mol-state/object.ts                  |  13 +-
 src/mol-state/selection.ts               |   4 +-
 src/mol-state/state.ts                   | 399 +++++++++++------------
 src/mol-state/transform.ts               |   4 +-
 src/mol-state/tree.ts                    |  77 +----
 src/mol-state/tree/base.ts               | 278 ----------------
 src/mol-state/tree/builder.ts            |  53 +++
 src/mol-state/tree/immutable.ts          | 164 ++++++++++
 src/mol-state/tree/transient.ts          | 177 ++++++++++
 src/perf-tests/state.ts                  |   2 +-
 14 files changed, 618 insertions(+), 570 deletions(-)
 delete mode 100644 src/mol-state/tree/base.ts
 create mode 100644 src/mol-state/tree/builder.ts
 create mode 100644 src/mol-state/tree/immutable.ts
 create mode 100644 src/mol-state/tree/transient.ts

diff --git a/src/mol-plugin/behavior/built-in/data.ts b/src/mol-plugin/behavior/built-in/data.ts
index e8107d0fc..e472205f5 100644
--- a/src/mol-plugin/behavior/built-in/data.ts
+++ b/src/mol-plugin/behavior/built-in/data.ts
@@ -6,7 +6,6 @@
 
 import { PluginBehavior } from '../behavior';
 import { PluginCommands } from 'mol-plugin/command';
-import { StateTree } from 'mol-state';
 
 export const SetCurrentObject = PluginBehavior.create({
     name: 'set-current-data-object-behavior',
@@ -23,7 +22,7 @@ export const Update = PluginBehavior.create({
 export const RemoveObject = PluginBehavior.create({
     name: 'remove-object-data-behavior',
     ctor: PluginBehavior.simpleCommandHandler(PluginCommands.Data.RemoveObject, ({ ref }, ctx) => {
-        const tree = StateTree.build(ctx.state.data.tree).delete(ref).getTree();
+        const tree = ctx.state.data.tree.build().delete(ref).getTree();
         ctx.state.updateData(tree);
     }),
     display: { name: 'Remove Object Handler', group: 'Data' }
diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts
index ced63f9a5..b59d2610f 100644
--- a/src/mol-plugin/context.ts
+++ b/src/mol-plugin/context.ts
@@ -83,7 +83,7 @@ export class PluginContext {
     }
 
     async _test_initBehaviours() {
-        const tree = StateTree.build(this.state.behavior.tree)
+        const tree = this.state.behavior.tree.build()
             .toRoot().apply(PluginBehaviors.Data.SetCurrentObject, { ref: PluginBehaviors.Data.SetCurrentObject.id })
             .and().toRoot().apply(PluginBehaviors.Data.Update, { ref: PluginBehaviors.Data.Update.id })
             .and().toRoot().apply(PluginBehaviors.Data.RemoveObject, { ref: PluginBehaviors.Data.RemoveObject.id })
@@ -96,7 +96,7 @@ export class PluginContext {
     }
 
     _test_applyTransform(a: Transform.Ref, transformer: Transformer, params: any) {
-        const tree = StateTree.build(this.state.data.tree).to(a).apply(transformer, params).getTree();
+        const tree = this.state.data.tree.build().to(a).apply(transformer, params).getTree();
         PluginCommands.Data.Update.dispatch(this, { tree });
     }
 
@@ -106,7 +106,7 @@ export class PluginContext {
     }
 
     _test_createState(url: string) {
-        const b = StateTree.build(this.state.data.tree);
+        const b = this.state.data.tree.build();
 
         const query = MolScriptBuilder.struct.generator.atomGroups({
             // 'atom-test': MolScriptBuilder.core.rel.eq([
diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts
index 80cc88b03..6ce9eccf6 100644
--- a/src/mol-plugin/state.ts
+++ b/src/mol-plugin/state.ts
@@ -26,8 +26,8 @@ class PluginState {
     }
 
     async setSnapshot(snapshot: PluginState.Snapshot) {
-        await this.behavior.setSnapshot(snapshot.behaviour);
-        await this.data.setSnapshot(snapshot.data);
+        await this.plugin.runTask(this.behavior.setSnapshot(snapshot.behaviour));
+        await this.plugin.runTask(this.data.setSnapshot(snapshot.data));
         this.plugin.canvas3d.camera.setState(snapshot.canvas3d.camera);
         this.plugin.canvas3d.requestDraw(true);
     }
diff --git a/src/mol-state/context.ts b/src/mol-state/context.ts
index 67641d7be..7680623c0 100644
--- a/src/mol-state/context.ts
+++ b/src/mol-state/context.ts
@@ -34,15 +34,13 @@ class StateContext {
     };
 
     readonly globalContext: unknown;
-    readonly defaultCellState: unknown;
 
     dispose() {
         this.ev.dispose();
     }
 
-    constructor(params: { globalContext: unknown, defaultCellState: unknown }) {
+    constructor(params: { globalContext: unknown }) {
         this.globalContext = params.globalContext;
-        this.defaultCellState = params.defaultCellState;
         this.behaviors.currentObject.next({ ref: Transform.RootRef });
     }
 }
\ No newline at end of file
diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts
index 14f759114..831885b05 100644
--- a/src/mol-state/object.ts
+++ b/src/mol-state/object.ts
@@ -50,9 +50,16 @@ namespace StateObjectCell {
     export type Status = 'ok' | 'error' | 'pending' | 'processing'
 
     export interface State {
-        isVisible: boolean,
+        isObjectHidden: boolean,
         isHidden: boolean,
-        isBound: boolean,
-        isExpanded: boolean
+        isBinding: boolean,
+        isCollapsed: boolean
     }
+
+    export const DefaultState: State = {
+        isObjectHidden: false,
+        isHidden: false,
+        isBinding: false,
+        isCollapsed: false
+    };
 }
\ No newline at end of file
diff --git a/src/mol-state/selection.ts b/src/mol-state/selection.ts
index e2863f368..47014c62f 100644
--- a/src/mol-state/selection.ts
+++ b/src/mol-state/selection.ts
@@ -6,7 +6,7 @@
 
 import { StateObject, StateObjectCell } from './object';
 import { State } from './state';
-import { ImmutableTree } from './tree';
+import { StateTree } from './tree';
 import { Transform } from './transform';
 
 namespace StateSelection {
@@ -150,7 +150,7 @@ namespace StateSelection {
     export function subtree(b: Selector) {
         return flatMap(b, (n, s) => {
             const nodes = [] as string[];
-            ImmutableTree.doPreOrder(s.tree, s.tree.nodes.get(n.ref), nodes, (x, _, ctx) => { ctx.push(x.ref) });
+            StateTree.doPreOrder(s.tree, s.tree.nodes.get(n.ref), nodes, (x, _, ctx) => { ctx.push(x.ref) });
             return nodes.map(x => s.cells.get(x)!);
         });
     }
diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts
index 5c9aecdf5..f1b04ccb7 100644
--- a/src/mol-state/state.ts
+++ b/src/mol-state/state.ts
@@ -5,17 +5,18 @@
  */
 
 import { StateObject, StateObjectCell } from './object';
-import { StateTree, ImmutableTree } from './tree';
+import { StateTree } from './tree';
 import { Transform } from './transform';
 import { Transformer } from './transformer';
 import { StateContext } from './context';
 import { UUID } from 'mol-util';
 import { RuntimeContext, Task } from 'mol-task';
+import { StateSelection } from './selection';
 
 export { State }
 
 class State {
-    private _tree: StateTree = StateTree.create();
+    private _tree: StateTree = StateTree.createEmpty();
     private _current: Transform.Ref = this._tree.root.ref;
     private transformCache = new Map<Transform.Ref, unknown>();
 
@@ -26,24 +27,12 @@ class State {
     readonly context: StateContext;
 
     getSnapshot(): State.Snapshot {
-        const props = Object.create(null);
-        const keys = this.cells.keys();
-        while (true) {
-            const key = keys.next();
-            if (key.done) break;
-            const o = this.cells.get(key.value)!;
-            props[key.value] = { ...o.state };
-        }
-        return {
-            tree: StateTree.toJSON(this._tree),
-            props
-        };
+        return { tree: StateTree.toJSON(this._tree) };
     }
 
     setSnapshot(snapshot: State.Snapshot) {
         const tree = StateTree.fromJSON(snapshot.tree);
-        // TODO: support props and async
-        return this.update(tree).run();
+        return this.update(tree);
     }
 
     setCurrent(ref: Transform.Ref) {
@@ -51,10 +40,22 @@ class State {
         this.context.behaviors.currentObject.next({ ref });
     }
 
+    updateCellState(ref: Transform.Ref, state?: Partial<StateObjectCell.State>) {
+        // TODO
+    }
+
     dispose() {
         this.context.dispose();
     }
 
+    select(selector: StateSelection.Selector) {
+        return StateSelection.select(selector, this);
+    }
+
+    get selector() {
+        return StateSelection;
+    }
+
     update(tree: StateTree): Task<void> {
         // TODO: support props
         return Task.create('Update Tree', async taskCtx => {
@@ -78,22 +79,20 @@ class State {
         });
     }
 
-    constructor(rootObject: StateObject, params?: { globalContext?: unknown, defaultCellState?: unknown }) {
+    constructor(rootObject: StateObject, params?: { globalContext?: unknown }) {
         const tree = this._tree;
         const root = tree.root;
-        const defaultCellState = (params && params.defaultCellState) || { }
 
         this.cells.set(root.ref, {
             ref: root.ref,
             obj: rootObject,
             status: 'ok',
             version: root.version,
-            state: { ...defaultCellState }
+            state: { ...StateObjectCell.DefaultState }
         });
 
         this.context = new StateContext({
-            globalContext: params && params.globalContext,
-            defaultCellState
+            globalContext: params && params.globalContext
         });
     }
 }
@@ -102,8 +101,7 @@ namespace State {
     export type Cells = Map<Transform.Ref, StateObjectCell>
 
     export interface Snapshot {
-        readonly tree: StateTree.Serialized,
-        readonly props: { [key: string]: unknown }
+        readonly tree: StateTree.Serialized
     }
 
     export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) {
@@ -111,212 +109,213 @@ namespace State {
     }
 }
 
-    type Ref = Transform.Ref
+type Ref = Transform.Ref
 
-    interface UpdateContext {
-        stateCtx: StateContext,
-        taskCtx: RuntimeContext,
-        oldTree: StateTree,
-        tree: StateTree,
-        cells: State.Cells,
-        transformCache: Map<Ref, unknown>
-    }
+interface UpdateContext {
+    stateCtx: StateContext,
+    taskCtx: RuntimeContext,
+    oldTree: StateTree,
+    tree: StateTree,
+    cells: State.Cells,
+    transformCache: Map<Ref, unknown>
+}
 
-    async function update(ctx: UpdateContext) {
-        const roots = findUpdateRoots(ctx.cells, ctx.tree);
-        const deletes = findDeletes(ctx);
-        for (const d of deletes) {
-            const obj = ctx.cells.has(d) ? ctx.cells.get(d)!.obj : void 0;
-            ctx.cells.delete(d);
-            ctx.transformCache.delete(d);
-            ctx.stateCtx.events.object.removed.next({ ref: d, obj });
-            // TODO: handle current object change
-        }
+async function update(ctx: UpdateContext) {
+    const roots = findUpdateRoots(ctx.cells, ctx.tree);
+    const deletes = findDeletes(ctx);
+    for (const d of deletes) {
+        const obj = ctx.cells.has(d) ? ctx.cells.get(d)!.obj : void 0;
+        ctx.cells.delete(d);
+        ctx.transformCache.delete(d);
+        ctx.stateCtx.events.object.removed.next({ ref: d, obj });
+        // TODO: handle current object change
+    }
 
-        initObjectState(ctx, roots);
+    initCells(ctx, roots);
+    initObjectStatus(ctx, roots);
 
-        for (const root of roots) {
-            await updateSubtree(ctx, root);
-        }
+    for (const root of roots) {
+        await updateSubtree(ctx, root);
     }
+}
 
-    function findUpdateRoots(cells: State.Cells, tree: StateTree) {
-        const findState = { roots: [] as Ref[], cells };
-        ImmutableTree.doPreOrder(tree, tree.root, findState, _findUpdateRoots);
-        return findState.roots;
+function findUpdateRoots(cells: State.Cells, tree: StateTree) {
+    const findState = { roots: [] as Ref[], cells };
+    StateTree.doPreOrder(tree, tree.root, findState, _findUpdateRoots);
+    return findState.roots;
+}
+
+function _findUpdateRoots(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) {
+    const cell = s.cells.get(n.ref);
+    if (!cell || cell.version !== n.version) {
+        s.roots.push(n.ref);
+        return false;
     }
+    return true;
+}
 
-    function _findUpdateRoots(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) {
-        if (!s.cells.has(n.ref)) {
-            s.roots.push(n.ref);
-            return false;
-        }
-        const o = s.cells.get(n.ref)!;
-        if (o.version !== n.version) {
-            s.roots.push(n.ref);
-            return false;
-        }
+type FindDeletesCtx = { newTree: StateTree, cells: State.Cells, deletes: Ref[] }
+function _visitCheckDelete(n: Transform, _: any, ctx: FindDeletesCtx) {
+    if (!ctx.newTree.nodes.has(n.ref) && ctx.cells.has(n.ref)) ctx.deletes.push(n.ref);
+}
+function findDeletes(ctx: UpdateContext): Ref[] {
+    const deleteCtx: FindDeletesCtx = { newTree: ctx.tree, cells: ctx.cells, deletes: [] };
+    StateTree.doPostOrder(ctx.oldTree, ctx.oldTree.root, deleteCtx, _visitCheckDelete);
+    return deleteCtx.deletes;
+}
 
-        return true;
-    }
+function setObjectStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) {
+    const obj = ctx.cells.get(ref)!;
+    const changed = obj.status !== status;
+    obj.status = status;
+    obj.errorText = errorText;
+    if (changed) ctx.stateCtx.events.object.stateChanged.next({ ref });
+}
 
-    type FindDeletesCtx = { newTree: StateTree, cells: State.Cells, deletes: Ref[] }
-    function _visitCheckDelete(n: Transform, _: any, ctx: FindDeletesCtx) {
-        if (!ctx.newTree.nodes.has(n.ref) && ctx.cells.has(n.ref)) ctx.deletes.push(n.ref);
-    }
-    function findDeletes(ctx: UpdateContext): Ref[] {
-        const deleteCtx: FindDeletesCtx = { newTree: ctx.tree, cells: ctx.cells, deletes: [] };
-        ImmutableTree.doPostOrder(ctx.oldTree, ctx.oldTree.root, deleteCtx, _visitCheckDelete);
-        return deleteCtx.deletes;
-    }
+function _initObjectStatusVisitor(t: Transform, _: any, ctx: UpdateContext) {
+    setObjectStatus(ctx, t.ref, 'pending');
+}
 
-    function setObjectState(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) {
-        let changed = false;
-        if (ctx.cells.has(ref)) {
-            const obj = ctx.cells.get(ref)!;
-            changed = obj.status !== status;
-            obj.status = status;
-            obj.errorText = errorText;
-        } else {
-            const obj: StateObjectCell = { ref, status, version: UUID.create(), errorText, state: { ...ctx.stateCtx.defaultCellState } };
-            ctx.cells.set(ref, obj);
-            changed = true;
-        }
-        if (changed) ctx.stateCtx.events.object.stateChanged.next({ ref });
+/** Return "resolve set" */
+function initObjectStatus(ctx: UpdateContext, roots: Ref[]) {
+    for (const root of roots) {
+        StateTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, _initObjectStatusVisitor);
     }
+}
 
-    function _initVisitor(t: Transform, _: any, ctx: UpdateContext) {
-        setObjectState(ctx, t.ref, 'pending');
-    }
-    /** Return "resolve set" */
-    function initObjectState(ctx: UpdateContext, roots: Ref[]) {
-        for (const root of roots) {
-            ImmutableTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, _initVisitor);
-        }
+function _initCellsVisitor(transform: Transform, _: any, ctx: UpdateContext) {
+    if (ctx.cells.has(transform.ref)) return;
+
+    const obj: StateObjectCell = {
+        ref: transform.ref,
+        status: 'pending',
+        version: UUID.create(),
+        errorText: void 0,
+        state: { ...StateObjectCell.DefaultState, ...transform.cellState }
+    };
+    ctx.cells.set(transform.ref, obj);
+
+    // TODO: created event???
+}
+
+function initCells(ctx: UpdateContext, roots: Ref[]) {
+    for (const root of roots) {
+        StateTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, _initCellsVisitor);
     }
+}
 
-    function doError(ctx: UpdateContext, ref: Ref, errorText: string) {
-        setObjectState(ctx, ref, 'error', errorText);
-        const wrap = ctx.cells.get(ref)!;
-        if (wrap.obj) {
-            ctx.stateCtx.events.object.removed.next({ ref });
-            ctx.transformCache.delete(ref);
-            wrap.obj = void 0;
-        }
+function doError(ctx: UpdateContext, ref: Ref, errorText: string) {
+    setObjectStatus(ctx, ref, 'error', errorText);
+    const wrap = ctx.cells.get(ref)!;
+    if (wrap.obj) {
+        ctx.stateCtx.events.object.removed.next({ ref });
+        ctx.transformCache.delete(ref);
+        wrap.obj = void 0;
+    }
 
-        const children = ctx.tree.children.get(ref).values();
-        while (true) {
-            const next = children.next();
-            if (next.done) return;
-            doError(ctx, next.value, 'Parent node contains error.');
-        }
+    const children = ctx.tree.children.get(ref).values();
+    while (true) {
+        const next = children.next();
+        if (next.done) return;
+        doError(ctx, next.value, 'Parent node contains error.');
     }
+}
 
-    function findAncestor(tree: StateTree, cells: State.Cells, root: Ref, types: { type: StateObject.Type }[]): StateObject {
-        let current = tree.nodes.get(root)!;
-        while (true) {
-            current = tree.nodes.get(current.parent)!;
-            if (current.ref === Transform.RootRef) {
-                return cells.get(Transform.RootRef)!.obj!;
-            }
-            const obj = cells.get(current.ref)!.obj!;
-            for (const t of types) if (obj.type === t.type) return cells.get(current.ref)!.obj!;
+function findAncestor(tree: StateTree, cells: State.Cells, root: Ref, types: { type: StateObject.Type }[]): StateObject {
+    let current = tree.nodes.get(root)!;
+    while (true) {
+        current = tree.nodes.get(current.parent)!;
+        if (current.ref === Transform.RootRef) {
+            return cells.get(Transform.RootRef)!.obj!;
         }
+        const obj = cells.get(current.ref)!.obj!;
+        for (const t of types) if (obj.type === t.type) return cells.get(current.ref)!.obj!;
     }
+}
 
-    async function updateSubtree(ctx: UpdateContext, root: Ref) {
-        setObjectState(ctx, root, 'processing');
-
-        try {
-            const update = await updateNode(ctx, root);
-            setObjectState(ctx, root, 'ok');
-            if (update.action === 'created') {
-                ctx.stateCtx.events.object.created.next({ ref: root, obj: update.obj! });
-            } else if (update.action === 'updated') {
-                ctx.stateCtx.events.object.updated.next({ ref: root, obj: update.obj });
-            } else if (update.action === 'replaced') {
-                ctx.stateCtx.events.object.replaced.next({ ref: root, oldObj: update.oldObj, newObj: update.newObj });
-            }
-        } catch (e) {
-            doError(ctx, root, '' + e);
-            return;
+async function updateSubtree(ctx: UpdateContext, root: Ref) {
+    setObjectStatus(ctx, root, 'processing');
+
+    try {
+        const update = await updateNode(ctx, root);
+        setObjectStatus(ctx, root, 'ok');
+        if (update.action === 'created') {
+            ctx.stateCtx.events.object.created.next({ ref: root, obj: update.obj! });
+        } else if (update.action === 'updated') {
+            ctx.stateCtx.events.object.updated.next({ ref: root, obj: update.obj });
+        } else if (update.action === 'replaced') {
+            ctx.stateCtx.events.object.replaced.next({ ref: root, oldObj: update.oldObj, newObj: update.newObj });
         }
+    } catch (e) {
+        doError(ctx, root, '' + e);
+        return;
+    }
 
-        const children = ctx.tree.children.get(root).values();
-        while (true) {
-            const next = children.next();
-            if (next.done) return;
-            await updateSubtree(ctx, next.value);
-        }
+    const children = ctx.tree.children.get(root).values();
+    while (true) {
+        const next = children.next();
+        if (next.done) return;
+        await updateSubtree(ctx, next.value);
     }
+}
 
-    async function updateNode(ctx: UpdateContext, currentRef: Ref) {
-        const { oldTree, tree, cells } = ctx;
-        const transform = tree.nodes.get(currentRef);
-        const parent = findAncestor(tree, cells, currentRef, transform.transformer.definition.from);
-        // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined')
-        if (!oldTree.nodes.has(currentRef) || !cells.has(currentRef)) {
-            // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef));
-            const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
-            cells.set(currentRef, {
-                ref: currentRef,
-                obj,
-                status: 'ok',
-                version: transform.version,
-                state: { ...ctx.stateCtx.defaultCellState, ...transform.cellState }
-            });
-            return { action: 'created', obj };
-        } else {
-            // console.log('updating...', transform.transformer.id);
-            const current = cells.get(currentRef)!;
-            const oldParams = oldTree.nodes.get(currentRef)!.params;
-
-            const updateKind = current.status === 'ok' || current.ref === Transform.RootRef
-                ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params)
-                : Transformer.UpdateResult.Recreate;
-
-            switch (updateKind) {
-                case Transformer.UpdateResult.Recreate: {
-                    const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
-                    cells.set(currentRef, {
-                        ref: currentRef,
-                        obj,
-                        status: 'ok',
-                        version: transform.version,
-                        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.cellState };
-                    return { action: 'updated', obj: current.obj };
-                default:
-                    // TODO check if props need to be updated
-                    return { action: 'none' };
+async function updateNode(ctx: UpdateContext, currentRef: Ref) {
+    const { oldTree, tree, cells } = ctx;
+    const transform = tree.nodes.get(currentRef);
+    const parent = findAncestor(tree, cells, currentRef, transform.transformer.definition.from);
+    // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined')
+    if (!oldTree.nodes.has(currentRef) || !cells.has(currentRef)) {
+        // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef));
+        const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
+        const cell = cells.get(currentRef)!;
+        cell.obj = obj;
+        cell.version = transform.version;
+
+        return { action: 'created', obj };
+    } else {
+        const current = cells.get(currentRef)!;
+        const oldParams = oldTree.nodes.get(currentRef)!.params;
+
+        const updateKind = current.status === 'ok' || current.ref === Transform.RootRef
+            ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params)
+            : Transformer.UpdateResult.Recreate;
+
+        switch (updateKind) {
+            case Transformer.UpdateResult.Recreate: {
+                const oldObj = current.obj;
+                const newObj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
+                current.obj = newObj;
+                current.version = transform.version;
+                return { action: 'replaced', oldObj, newObj: newObj };
             }
+            case Transformer.UpdateResult.Updated:
+                current.version = transform.version;
+                return { action: 'updated', obj: current.obj };
+            default:
+                return { action: 'none' };
         }
     }
+}
 
-    function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
-        if (typeof (t as any).run === 'function') return (t as Task<T>).runInContext(ctx);
-        return t as T;
-    }
+function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
+    if (typeof (t as any).runInContext === 'function') return (t as Task<T>).runInContext(ctx);
+    return t as T;
+}
 
-    function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) {
-        const cache = { };
+function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) {
+    const cache = {};
+    ctx.transformCache.set(ref, cache);
+    return runTask(transformer.definition.apply({ a, params, cache }, ctx.stateCtx.globalContext), ctx.taskCtx);
+}
+
+async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
+    if (!transformer.definition.update) {
+        return Transformer.UpdateResult.Recreate;
+    }
+    let cache = ctx.transformCache.get(ref);
+    if (!cache) {
+        cache = {};
         ctx.transformCache.set(ref, cache);
-        return runTask(transformer.definition.apply({ a, params, cache }, ctx.stateCtx.globalContext), ctx.taskCtx);
     }
-
-    async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
-        if (!transformer.definition.update) {
-            return Transformer.UpdateResult.Recreate;
-        }
-        let cache = ctx.transformCache.get(ref);
-        if (!cache) {
-            cache = { };
-            ctx.transformCache.set(ref, cache);
-        }
-        return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.stateCtx.globalContext), ctx.taskCtx);
-    }
\ No newline at end of file
+    return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.stateCtx.globalContext), ctx.taskCtx);
+}
\ No newline at end of file
diff --git a/src/mol-state/transform.ts b/src/mol-state/transform.ts
index 95ed6cd8c..02b3b7ab2 100644
--- a/src/mol-state/transform.ts
+++ b/src/mol-state/transform.ts
@@ -40,8 +40,8 @@ export namespace Transform {
         return { ...t, params, version: UUID.create() };
     }
 
-    export function createRoot(ref: Ref): Transform {
-        return create(RootRef, Transformer.ROOT, {}, { ref });
+    export function createRoot(): Transform {
+        return create(RootRef, Transformer.ROOT, {}, { ref: RootRef });
     }
 
     export interface Serialized {
diff --git a/src/mol-state/tree.ts b/src/mol-state/tree.ts
index d13575dbe..837524f80 100644
--- a/src/mol-state/tree.ts
+++ b/src/mol-state/tree.ts
@@ -4,78 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Transform } from './transform';
-import { ImmutableTree, TransientTree } from './tree/base';
-import { Transformer } from './transformer';
-import { StateObject } from './object';
+import { StateTree } from './tree/immutable';
+import { TransientTree } from './tree/transient';
 
-export { StateTree, ImmutableTree, TransientTree }
-
-interface StateTree extends ImmutableTree { }
-
-namespace StateTree {
-    export interface Transient extends TransientTree { }
-    export interface Serialized extends ImmutableTree.Serialized { }
-
-    export function create() {
-        return ImmutableTree.create(Transform.createRoot(Transform.RootRef));
-    }
-
-    export function updateParams<T extends Transformer = Transformer>(tree: StateTree, ref: Transform.Ref, params: Transformer.Params<T>): StateTree {
-        const t = tree.nodes.get(ref)!;
-        const newTransform = Transform.updateParams(t, params);
-        return tree.asTransient().set(newTransform).asImmutable();
-    }
-
-    export function toJSON(tree: StateTree) {
-        return ImmutableTree.toJSON(tree) as Serialized;
-    }
-
-    export function fromJSON(data: Serialized): StateTree {
-        return ImmutableTree.fromJSON(data);
-    }
-
-    export interface Builder {
-        getTree(): StateTree
-    }
-
-    export function build(tree: StateTree) {
-        return new Builder.Root(tree);
-    }
-
-    export namespace Builder {
-        interface State {
-            tree: StateTree.Transient
-        }
-
-        export class Root implements Builder {
-            private state: State;
-            to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref, this); }
-            toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); }
-            delete(ref: Transform.Ref) {
-                this.state.tree.remove(ref);
-                return this;
-            }
-            getTree(): StateTree { return this.state.tree.asImmutable(); }
-            constructor(tree: StateTree) { this.state = { tree: tree.asTransient() } }
-        }
-
-        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(this.ref, params, props);
-                this.state.tree.add(t);
-                return new To(this.state, t.ref, this.root);
-            }
-
-            and() { return this.root; }
-
-            getTree(): StateTree { return this.state.tree.asImmutable(); }
-
-            constructor(private state: State, private ref: Transform.Ref, private root: Root) {
-                if (!this.state.tree.nodes.has(ref)) {
-                    throw new Error(`Could not find node '${ref}'.`);
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
+export { StateTree, TransientTree }
\ No newline at end of file
diff --git a/src/mol-state/tree/base.ts b/src/mol-state/tree/base.ts
deleted file mode 100644
index ca44d05d5..000000000
--- a/src/mol-state/tree/base.ts
+++ /dev/null
@@ -1,278 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { Map as ImmutableMap, OrderedSet } from 'immutable';
-import { Transform } from '../transform';
-
-export { ImmutableTree, TransientTree }
-
-/**
- * An immutable tree where each node requires a unique reference.
- * Represented as an immutable map.
- */
-interface ImmutableTree {
-    readonly version: number,
-    readonly root: Transform,
-    readonly nodes: ImmutableTree.Nodes,
-    readonly children: ImmutableTree.Children,
-    asTransient(): TransientTree
-}
-
-namespace ImmutableTree {
-    type Ref = Transform.Ref
-
-    export interface ChildSet {
-        readonly size: number,
-        readonly values: OrderedSet<Ref>['values'],
-        has(ref: Ref): boolean,
-        readonly forEach: OrderedSet<Ref>['forEach'],
-        readonly map: OrderedSet<Ref>['map']
-    }
-
-    export type Node = Transform
-    export type Nodes = ImmutableMap<Ref, Transform>
-    export type Children = ImmutableMap<Ref, ChildSet>
-
-    class Impl implements ImmutableTree {
-        get root() { return this.nodes.get(Transform.RootRef)! }
-
-        asTransient(): TransientTree {
-            return new TransientTree(this);
-        }
-
-        constructor(public nodes: ImmutableTree.Nodes, public children: Children, public version: number) {
-        }
-    }
-
-    /**
-     * Create an instance of an immutable tree.
-     */
-    export function create(root: Transform): ImmutableTree {
-        return new Impl(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]), 0);
-    }
-
-    export function construct(nodes: Nodes, children: Children, version: number): ImmutableTree {
-        return new Impl(nodes, children, version);
-    }
-
-    type VisitorCtx = { tree: ImmutableTree, state: any, f: (node: Node, tree: ImmutableTree, state: any) => boolean | undefined | void };
-
-    function _postOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPostOrder(this, this.tree.nodes.get(c!)); }
-    function _doPostOrder(ctx: VisitorCtx, root: Node) {
-        const children = ctx.tree.children.get(root.ref);
-        if (checkSetRef && children.size) {
-            children.forEach(_postOrderFunc, ctx);
-        }
-        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, 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: Ref | undefined) { _doPreOrder(this, this.tree.nodes.get(c!)); }
-    function _doPreOrder(ctx: VisitorCtx, root: Node) {
-        const ret = ctx.f(root, ctx.tree, ctx.state);
-        if (typeof ret === 'boolean' && !ret) return;
-        const children = ctx.tree.children.get(root.ref);
-        if (checkSetRef && children.size) {
-            children.forEach(_preOrderFunc, ctx);
-        }
-    }
-
-    /**
-     * 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, tree: ImmutableTree, state: S) => boolean | undefined | void): S {
-        const ctx: VisitorCtx = { tree, state, f };
-        _doPreOrder(ctx, root);
-        return ctx.state;
-    }
-
-    function _subtree(n: Node, _: any, subtree: Node[]) { subtree.push(n); }
-    /**
-     * Get all nodes in a subtree, leafs come first.
-     */
-    export function subtreePostOrder<T>(tree: ImmutableTree, root: Node) {
-        return doPostOrder<Node[]>(tree, root, [], _subtree);
-    }
-
-
-    function _visitChildToJson(this: Ref[], ref: Ref) { this.push(ref); }
-    interface ToJsonCtx { tree: ImmutableTree, nodes: [Transform.Serialized, any[]][] }
-    function _visitNodeToJson(this: ToJsonCtx, node: Node) {
-        const children: Ref[] = [];
-        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[] /** children indices */][]
-    }
-
-    export function toJSON<T>(tree: ImmutableTree): Serialized {
-        const ctx: ToJsonCtx = { tree, nodes: [] };
-
-        tree.nodes.forEach(_visitNodeToJson as any, ctx);
-
-        const map = new Map<string, number>();
-        let i = 0;
-        for (const n of ctx.nodes) map.set(n[0].ref, i++);
-
-        for (const n of ctx.nodes) {
-            const children = n[1];
-            for (i = 0; i < children.length; i++) {
-                children[i] = map.get(children[i]);
-            }
-        }
-        return {
-            nodes: ctx.nodes
-        };
-    }
-
-    export function fromJSON<T>(data: Serialized): ImmutableTree {
-        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);
-            children.set(ref, OrderedSet(node[1].map(c => values[c].ref)));
-        }
-        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 changedValue = false;
-    private mutations: Map<Transform.Ref, OrderedSet<Transform.Ref>> = new Map();
-
-    get root() { return this.nodes.get(Transform.RootRef)! }
-
-    asTransient() {
-        return this.asImmutable().asTransient();
-    }
-
-    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);
-        }
-    }
-
-    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);
-        }
-    }
-
-    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);
-    }
-
-    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());
-        }
-
-        this.nodes.set(ref, transform);
-        return this;
-    }
-
-    set(transform: Transform) {
-        ensurePresent(this.nodes, transform.ref);
-        this.changedValue = true;
-        this.nodes.set(transform.ref, transform);
-        return this;
-    }
-
-    remove(ref: Transform.Ref): Transform[] {
-        const { nodes, mutations, children } = this;
-        const node = nodes.get(ref);
-        if (!node) return [];
-
-        const st = ImmutableTree.subtreePostOrder(this, node);
-        if (ref === Transform.RootRef) {
-            st.pop();
-            this.clearRoot();
-        } else {
-            this.removeChild(node.parent, node.ref);
-        }
-
-        for (const n of st) {
-            nodes.delete(n.ref);
-            children.delete(n.ref);
-            mutations.delete(n.ref);
-        }
-
-        return st;
-    }
-
-    asImmutable() {
-        if (!this.changedValue && this.mutations.size === 0) return this.tree;
-
-        this.mutations.forEach(fixChildMutations, this.children);
-        return ImmutableTree.construct(this.nodes.asImmutable(), this.children.asImmutable(), this.version);
-    }
-
-    constructor(private tree: ImmutableTree) {
-
-    }
-}
-
-function fixChildMutations(this: ImmutableTree.Children, m: OrderedSet<Transform.Ref>, k: Transform.Ref) { this.set(k, m.asImmutable()); }
-
-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 parentNotPresent(ref: Transform.Ref) {
-    throw new Error(`Parent '${ref}' must be present in the tree.`);
-}
-
-function ensurePresent(nodes: ImmutableTree.Nodes, ref: Transform.Ref) {
-    if (!nodes.has(ref)) {
-        throw new Error(`Node '${ref}' is not present in the tree.`);
-    }
-}
\ No newline at end of file
diff --git a/src/mol-state/tree/builder.ts b/src/mol-state/tree/builder.ts
new file mode 100644
index 000000000..9f2f6dc5c
--- /dev/null
+++ b/src/mol-state/tree/builder.ts
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StateTree } from './immutable';
+import { TransientTree } from './transient';
+import { StateObject } from '../object';
+import { Transform } from '../transform';
+import { Transformer } from '../transformer';
+
+export { StateTreeBuilder }
+
+interface StateTreeBuilder {
+    getTree(): StateTree
+}
+
+namespace StateTreeBuilder {
+    interface State {
+        tree: TransientTree
+    }
+
+    export class Root implements StateTreeBuilder {
+        private state: State;
+        to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref, this); }
+        toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); }
+        delete(ref: Transform.Ref) {
+            this.state.tree.remove(ref);
+            return this;
+        }
+        getTree(): StateTree { return this.state.tree.asImmutable(); }
+        constructor(tree: StateTree) { this.state = { tree: tree.asTransient() } }
+    }
+
+    export class To<A extends StateObject> implements StateTreeBuilder {
+        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(this.ref, params, props);
+            this.state.tree.add(t);
+            return new To(this.state, t.ref, this.root);
+        }
+
+        and() { return this.root; }
+
+        getTree(): StateTree { return this.state.tree.asImmutable(); }
+
+        constructor(private state: State, private ref: Transform.Ref, private root: Root) {
+            if (!this.state.tree.nodes.has(ref)) {
+                throw new Error(`Could not find node '${ref}'.`);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-state/tree/immutable.ts b/src/mol-state/tree/immutable.ts
new file mode 100644
index 000000000..c7832d3d8
--- /dev/null
+++ b/src/mol-state/tree/immutable.ts
@@ -0,0 +1,164 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Map as ImmutableMap, OrderedSet } from 'immutable';
+import { Transform } from '../transform';
+import { TransientTree } from './transient';
+import { StateTreeBuilder } from './builder';
+import { Transformer } from '../transformer';
+
+export { StateTree }
+
+/**
+ * An immutable tree where each node requires a unique reference.
+ * Represented as an immutable map.
+ */
+interface StateTree {
+    readonly root: Transform,
+    readonly nodes: StateTree.Nodes,
+    readonly children: StateTree.Children,
+    asTransient(): TransientTree,
+    build(): StateTreeBuilder.Root
+}
+
+namespace StateTree {
+    type Ref = Transform.Ref
+
+    export interface ChildSet {
+        readonly size: number,
+        readonly values: OrderedSet<Ref>['values'],
+        has(ref: Ref): boolean,
+        readonly forEach: OrderedSet<Ref>['forEach'],
+        readonly map: OrderedSet<Ref>['map']
+    }
+
+    export type Node = Transform
+    export type Nodes = ImmutableMap<Ref, Transform>
+    export type Children = ImmutableMap<Ref, ChildSet>
+
+    class Impl implements StateTree {
+        get root() { return this.nodes.get(Transform.RootRef)! }
+
+        asTransient(): TransientTree {
+            return new TransientTree(this);
+        }
+
+        build(): StateTreeBuilder.Root {
+            return new StateTreeBuilder.Root(this);
+        }
+
+        constructor(public nodes: StateTree.Nodes, public children: Children) {
+        }
+    }
+
+    /**
+     * Create an instance of an immutable tree.
+     */
+    export function createEmpty(): StateTree {
+        const root = Transform.createRoot();
+        return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]));
+    }
+
+    export function create(nodes: Nodes, children: Children): StateTree {
+        return new Impl(nodes, children);
+    }
+
+    export function updateParams<T extends Transformer = Transformer>(tree: StateTree, ref: Transform.Ref, params: Transformer.Params<T>): StateTree {
+        const t = tree.nodes.get(ref)!;
+        const newTransform = Transform.updateParams(t, params);
+        return tree.asTransient().set(newTransform).asImmutable();
+    }
+
+    type VisitorCtx = { tree: StateTree, state: any, f: (node: Node, tree: StateTree, state: any) => boolean | undefined | void };
+
+    function _postOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPostOrder(this, this.tree.nodes.get(c!)); }
+    function _doPostOrder(ctx: VisitorCtx, root: Node) {
+        const children = ctx.tree.children.get(root.ref);
+        if (children && children.size) {
+            children.forEach(_postOrderFunc, ctx);
+        }
+        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: StateTree, root: Node, state: S, f: (node: Node, tree: StateTree, state: S) => boolean | undefined | void): S {
+        const ctx: VisitorCtx = { tree, state, f };
+        _doPostOrder(ctx, root);
+        return ctx.state;
+    }
+
+    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.tree, ctx.state);
+        if (typeof ret === 'boolean' && !ret) return;
+        const children = ctx.tree.children.get(root.ref);
+        if (children && children.size) {
+            children.forEach(_preOrderFunc, ctx);
+        }
+    }
+
+    /**
+     * 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: StateTree, root: Node, state: S, f: (node: Node, tree: StateTree, state: S) => boolean | undefined | void): S {
+        const ctx: VisitorCtx = { tree, state, f };
+        _doPreOrder(ctx, root);
+        return ctx.state;
+    }
+
+    function _subtree(n: Node, _: any, subtree: Node[]) { subtree.push(n); }
+    /**
+     * Get all nodes in a subtree, leafs come first.
+     */
+    export function subtreePostOrder<T>(tree: StateTree, root: Node) {
+        return doPostOrder<Node[]>(tree, root, [], _subtree);
+    }
+
+
+    // function _visitChildToJson(this: Ref[], ref: Ref) { this.push(ref); }
+    // interface ToJsonCtx { nodes: Transform.Serialized[] }
+    function _visitNodeToJson(node: Node, tree: StateTree, ctx: Transform.Serialized[]) {
+        // const children: Ref[] = [];
+        // tree.children.get(node.ref).forEach(_visitChildToJson as any, children);
+        ctx.push(Transform.toJSON(node));
+    }
+
+    export interface Serialized {
+        /** Tree nodes serialized in pre-order */
+        nodes: Transform.Serialized[]
+    }
+
+    export function toJSON<T>(tree: StateTree): Serialized {
+        const nodes: Transform.Serialized[] = [];
+        doPreOrder(tree, tree.root, nodes, _visitNodeToJson);
+        return { nodes };
+    }
+
+    export function fromJSON<T>(data: Serialized): StateTree {
+        const nodes = ImmutableMap<Ref, Node>().asMutable();
+        const children = ImmutableMap<Ref, OrderedSet<Ref>>().asMutable();
+
+        for (const s of data.nodes) {
+            const node = Transform.fromJSON(s);
+            nodes.set(node.ref, node);
+
+            if (!children.has(node.ref)) {
+                children.set(node.ref, OrderedSet<Ref>().asMutable());
+            }
+
+            if (node.ref !== node.parent) children.get(node.parent).add(node.ref);
+        }
+
+        for (const s of data.nodes) {
+            children.set(s.ref, children.get(s.ref).asImmutable());
+        }
+
+        return create(nodes.asImmutable(), children.asImmutable());
+    }
+}
\ No newline at end of file
diff --git a/src/mol-state/tree/transient.ts b/src/mol-state/tree/transient.ts
new file mode 100644
index 000000000..6fa34fe35
--- /dev/null
+++ b/src/mol-state/tree/transient.ts
@@ -0,0 +1,177 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { OrderedSet } from 'immutable';
+import { Transform } from '../transform';
+import { StateTree } from './immutable';
+import { StateTreeBuilder } from './builder';
+
+export { TransientTree }
+
+class TransientTree implements StateTree {
+    nodes = this.tree.nodes;
+    children = this.tree.children;
+
+    private changedNodes = false;
+    private changedChildren = false;
+    private _mutations: Map<Transform.Ref, OrderedSet<Transform.Ref>> = void 0 as any;
+
+    private get mutations() {
+        if (this._mutations) return this._mutations;
+        this._mutations = new Map();
+        return this._mutations;
+    }
+
+    get root() { return this.nodes.get(Transform.RootRef)! }
+
+    build(): StateTreeBuilder.Root {
+        return new StateTreeBuilder.Root(this);
+    }
+
+    asTransient() {
+        return this.asImmutable().asTransient();
+    }
+
+    private addChild(parent: Transform.Ref, child: Transform.Ref) {
+        if (!this.changedChildren) {
+            this.changedChildren = true;
+            this.children = this.children.asMutable();
+        }
+
+        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);
+        }
+    }
+
+    private removeChild(parent: Transform.Ref, child: Transform.Ref) {
+        if (!this.changedChildren) {
+            this.changedChildren = true;
+            this.children = this.children.asMutable();
+        }
+
+        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);
+        }
+    }
+
+    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);
+    }
+
+    add(transform: Transform) {
+        const ref = transform.ref;
+
+        if (this.nodes.has(transform.ref)) {
+            const node = this.nodes.get(transform.ref);
+            if (node.parent !== transform.parent) alreadyPresent(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)) {
+            if (!this.changedChildren) {
+                this.changedChildren = true;
+                this.children = this.children.asMutable();
+            }
+            this.children.set(transform.ref, OrderedSet());
+        }
+
+        if (!this.changedNodes) {
+            this.changedNodes = true;
+            this.nodes = this.nodes.asMutable();
+        }
+
+        this.nodes.set(ref, transform);
+        return this;
+    }
+
+    set(transform: Transform) {
+        ensurePresent(this.nodes, transform.ref);
+
+        if (!this.changedNodes) {
+            this.changedNodes = true;
+            this.nodes = this.nodes.asMutable();
+        }
+
+        this.nodes.set(transform.ref, transform);
+        return this;
+    }
+
+    remove(ref: Transform.Ref): Transform[] {
+        const node = this.nodes.get(ref);
+        if (!node) return [];
+
+        const st = StateTree.subtreePostOrder(this, node);
+        if (ref === Transform.RootRef) {
+            st.pop();
+            if (st.length === 0) return st;
+            this.clearRoot();
+        } else {
+            if (st.length === 0) return st;
+            this.removeChild(node.parent, node.ref);
+        }
+
+        if (!this.changedNodes) {
+            this.changedNodes = true;
+            this.nodes = this.nodes.asMutable();
+        }
+
+        for (const n of st) {
+            this.nodes.delete(n.ref);
+            this.children.delete(n.ref);
+            if (this._mutations) this._mutations.delete(n.ref);
+        }
+
+        return st;
+    }
+
+    asImmutable() {
+        if (!this.changedNodes && !this.changedChildren && !this._mutations) return this.tree;
+        if (this._mutations) this._mutations.forEach(fixChildMutations, this.children);
+        return StateTree.create(
+            this.changedNodes ? this.nodes.asImmutable() : this.nodes,
+            this.changedChildren ? this.children.asImmutable() : this.children);
+    }
+
+    constructor(private tree: StateTree) {
+
+    }
+}
+
+function fixChildMutations(this: StateTree.Children, m: OrderedSet<Transform.Ref>, k: Transform.Ref) { this.set(k, m.asImmutable()); }
+
+function alreadyPresent(ref: Transform.Ref) {
+    throw new Error(`Transform '${ref}' is already present in the tree.`);
+}
+
+function parentNotPresent(ref: Transform.Ref) {
+    throw new Error(`Parent '${ref}' must be present in the tree.`);
+}
+
+function ensurePresent(nodes: StateTree.Nodes, ref: Transform.Ref) {
+    if (!nodes.has(ref)) {
+        throw new Error(`Node '${ref}' is not present in the tree.`);
+    }
+}
\ No newline at end of file
diff --git a/src/perf-tests/state.ts b/src/perf-tests/state.ts
index 11ddf2094..d141d85fa 100644
--- a/src/perf-tests/state.ts
+++ b/src/perf-tests/state.ts
@@ -77,7 +77,7 @@ export async function testState() {
     hookEvents(state);
 
     const tree = state.tree;
-    const builder = StateTree.build(tree);
+    const builder = tree.build();
     builder.toRoot<Root>()
         .apply(CreateSquare, { a: 10 }, { ref: 'square' })
         .apply(CaclArea);
-- 
GitLab