Explorar o código

mol-state: wip, 1st JSON serialization & update prototype

David Sehnal %!s(int64=6) %!d(string=hai) anos
pai
achega
f6ef6427b6

+ 37 - 10
src/mol-state/object.ts

@@ -1,22 +1,26 @@
+
 /**
  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
+import { Transform } from './tree/transform';
+
 /** A mutable state object */
-export interface StateObject<T extends StateObject.Type = any> {
-    '@type': T,
-    label: string,
-    version: number
+export interface StateObject<P = unknown, D = unknown> {
+    ref: Transform.Ref,
+    readonly type: StateObject.Type,
+    readonly props: P,
+    readonly data: D
 }
 
 export namespace StateObject {
-    export type TypeOf<T>
-        = T extends StateObject<infer X> ? [X]
-        : T extends [StateObject<infer X>] ? [X]
-        : T extends [StateObject<infer X>, StateObject<infer Y>] ? [X, Y]
-        : unknown[];
+    // export type TypeOf<T>
+    //     = T extends StateObject<infer X> ? [X]
+    //     : T extends [StateObject<infer X>] ? [X]
+    //     : T extends [StateObject<infer X>, StateObject<infer Y>] ? [X, Y]
+    //     : unknown[];
 
     export enum StateType {
         // The object has been successfully created
@@ -29,5 +33,28 @@ export namespace StateObject {
         Processing
     }
 
-    export type Type = string & { '@type': 'state-object-type' }
+    export interface Type<Info = any> {
+        kind: string,
+        info: Info
+    }
+
+    export function factory<TypeInfo, CommonProps>() {
+        return <D = { }, P = {}>(kind: string, info: TypeInfo) => create<P & CommonProps, D, TypeInfo>(kind, info);
+    }
+
+    export function create<Props, Data, TypeInfo>(kind: string, typeInfo: TypeInfo) {
+        const dataType: Type<TypeInfo> = { kind, info: typeInfo };
+        return class implements StateObject<Props, Data> {
+            static type = dataType;
+            type = dataType;
+            ref = 'not set' as Transform.Ref;
+            constructor(public props: Props, public data: Data) { }
+        }
+    }
+
+    export interface Wrapped {
+        obj: StateObject,
+        state: StateType,
+        version: string
+    }
 }

+ 128 - 5
src/mol-state/state.ts

@@ -8,15 +8,19 @@ import { StateObject } from './object';
 import { TransformTree } from './tree/tree';
 import { Transform } from './tree/transform';
 import { Map as ImmutableMap } from 'immutable';
-import { StateContext } from './context/context';
+// import { StateContext } from './context/context';
+import { ImmutableTree } from './util/immutable-tree';
+import { Transformer } from './transformer';
+import { Task } from 'mol-task';
 
 export interface State<ObjectProps = unknown> {
     definition: State.Definition<ObjectProps>,
-    objects: Map<Transform.InstanceId, StateObject>
+    objects: State.Objects
 }
 
 export namespace State {
-    export type ObjectProps<P> = ImmutableMap<Transform.InstanceId, P>
+    export type ObjectProps<P> = ImmutableMap<Transform.Ref, P>
+    export type Objects = Map<Transform.Ref, StateObject.Wrapped>
 
     export interface Definition<P = unknown> {
         tree: TransformTree,
@@ -24,7 +28,126 @@ export namespace State {
         props: ObjectProps<P>
     }
 
-    export async function update<P>(context: StateContext, old: State<P>, tree: Definition<P>, props?: ObjectProps<P>): Promise<State<P>> {
-        throw 'nyi';
+    export function create(): State {
+        const tree = TransformTree.create();
+        const objects: Objects = new Map();
+        const root = tree.getValue(tree.rootRef)!;
+
+        objects.set(tree.rootRef, { obj: void 0 as any, state: StateObject.StateType.Ok, version: root.version });
+
+        return {
+            definition: {
+                tree,
+                props: ImmutableMap()
+            },
+            objects
+        };
+    }
+
+    export async function update<P>(state: State<P>, tree: TransformTree, props?: ObjectProps<P>): Promise<State<P>> {
+        const roots = findUpdateRoots(state.objects, tree);
+        const deletes = findDeletes(state.objects, tree);
+        for (const d of deletes) {
+            state.objects.delete(d);
+        }
+
+        console.log('roots', roots);
+        for (const root of roots) {
+            await updateSubtree(state.definition.tree, tree, state.objects, root);
+        }
+
+        return {
+            definition: { tree, props: props || state.definition.props },
+            objects: state.objects
+        };
+    }
+
+    function findUpdateRoots(objects: Objects, tree: TransformTree) {
+        console.log(tree);
+        const findState = {
+            roots: [] as Transform.Ref[],
+            objects
+        };
+
+        ImmutableTree.doPreOrder(tree, tree.nodes.get(tree.rootRef)!, findState, (n, _, s) => {
+            if (!s.objects.has(n.ref)) {
+                console.log('missing', n.ref);
+                s.roots.push(n.ref);
+                return false;
+            }
+            const o = s.objects.get(n.ref)!;
+            if (o.version !== n.value.version) {
+                console.log('diff version', n.ref, n.value.version, o.version);
+                s.roots.push(n.ref);
+                return false;
+            }
+
+            return true;
+        });
+
+        return findState.roots;
+    }
+
+    function findDeletes(objects: Objects, tree: TransformTree): Transform.Ref[] {
+        // TODO
+        return [];
+    }
+
+    function findParent(tree: TransformTree, objects: Objects, root: Transform.Ref, types: { type: StateObject.Type }[]): StateObject {
+        let current = tree.nodes.get(root)!;
+        console.log('finding', types.map(t => t.type.kind));
+        while (true) {
+            current = tree.nodes.get(current.parent)!;
+            if (current.ref === tree.rootRef) return objects.get(tree.rootRef)!.obj;
+            const obj = objects.get(current.ref)!.obj;
+            console.log('current', obj.type.kind);
+            for (const t of types) if (obj.type === t.type) return objects.get(current.ref)!.obj;
+        }
+    }
+
+    async function updateSubtree(oldTree: TransformTree, tree: TransformTree, objects: Objects, root: Transform.Ref) {
+        await updateNode(oldTree, tree, objects, root);
+        const children = tree.nodes.get(root)!.children.values();
+        while (true) {
+            const next = children.next();
+            if (next.done) return;
+            await updateSubtree(oldTree, tree, objects, next.value);
+        }
+    }
+
+    async function updateNode(oldTree: TransformTree, tree: TransformTree, objects: Objects, root: Transform.Ref) {
+        const transform = tree.getValue(root)!;
+        const parent = findParent(tree, objects, root, transform.transformer.definition.from);
+        console.log('parent', parent ? parent.ref : 'undefined')
+        if (!oldTree.nodes.has(transform.ref) || !objects.has(transform.ref)) {
+            console.log('creating...', transform.transformer.id, oldTree.nodes.has(transform.ref), objects.has(transform.ref));
+            const obj = await createObject(transform.transformer, parent, transform.params);
+            obj.ref = transform.ref;
+            objects.set(root, { obj, state: StateObject.StateType.Ok, version: transform.version });
+        } else {
+            console.log('updating...', transform.transformer.id);
+            const current = objects.get(transform.ref)!.obj;
+            const oldParams = oldTree.getValue(transform.ref)!.params;
+            await updateObject(transform.transformer, parent, current, oldParams, transform.params);
+            const obj = objects.get(root)!;
+            obj.version = transform.version;
+        }
+    }
+
+    async function runTask<A>(t: A | Task<A>): Promise<A> {
+        if ((t as any).run) return await (t as Task<A>).run();
+        return t as A;
+    }
+
+    function createObject(transformer: Transformer, parent: StateObject, params: any) {
+        return runTask(transformer.definition.apply(parent, params, 0 as any));
+    }
+
+    async function updateObject(transformer: Transformer, parent: StateObject, obj: StateObject, oldParams: any, params: any) {
+        if (!transformer.definition.update) {
+            // TODO
+            throw 'nyi';
+        }
+        return transformer.definition.update!(parent, oldParams, obj, params, 0 as any);
     }
 }

+ 92 - 42
src/mol-state/transformer.ts

@@ -7,54 +7,104 @@
 import { Task } from 'mol-task';
 import { StateObject } from './object';
 import { TransformContext } from './tree/context';
+import { Transform } from './tree/transform';
 
-export interface Transformer<A extends StateObject, B extends StateObject, P = any> {
+export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
+    apply(params?: P, props?: Partial<Transform.Props>): Transform<A, B, P>,
     readonly id: Transformer.Id,
-    readonly name: string,
-    readonly namespace: string,
-    readonly description?: string,
-    readonly from: StateObject.Type[],
-    readonly to: StateObject.Type[],
-
-    /**
-     * Apply the actual transformation. It must be pure (i.e. with no side effects).
-     * Returns a task that produces the result of the result directly.
-     */
-    apply(a: A, params: P, context: TransformContext): Task<B> | B,
-
-    /**
-     * Attempts to update the entity in a non-destructive way.
-     * For example changing a color scheme of a visual does not require computing new geometry.
-     * Return/resolve to undefined if the update is not possible.
-     *
-     * The ability to resolve the task to undefined is present for "async updates" (i.e. containing an ajax call).
-     */
-    update?(a: A, b: B, newParams: P, context: TransformContext): Task<B | undefined> | B | undefined,
-
-    /** Check the parameters and return a list of errors if the are not valid. */
-    defaultParams?(a: A, context: TransformContext): P,
-
-    /** Specify default control descriptors for the parameters */
-    defaultControls?(a: A, context: TransformContext): Transformer.ControlsFor<P>,
-
-    /** Check the parameters and return a list of errors if the are not valid. */
-    validateParams?(a: A, params: P, context: TransformContext): string[] | undefined,
-
-    /** Optional custom parameter equality. Use deep structural equal by default. */
-    areParamsEqual?(oldParams: P, newParams: P): boolean,
-
-    /** Test if the transform can be applied to a given node */
-    isApplicable?(a: A, context: TransformContext): boolean,
-
-    /** By default, returns true */
-    isSerializable?(params: P): { isSerializable: true } | { isSerializable: false; reason: string },
-
-    /** Custom conversion to and from JSON */
-    customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P }
+    readonly definition: Transformer.Definition<A, B, P>
 }
 
 export namespace Transformer {
     export type Id = string & { '@type': 'transformer-id' }
     export type Params<T extends Transformer<any, any, any>> = T extends Transformer<any, any, infer P> ? P : unknown;
     export type ControlsFor<Props> = { [P in keyof Props]?: any }
+
+    export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
+        readonly name: string,
+        readonly namespace?: string,
+        readonly from: { type: StateObject.Type }[],
+        readonly to: { type: StateObject.Type }[],
+
+        /**
+         * Apply the actual transformation. It must be pure (i.e. with no side effects).
+         * Returns a task that produces the result of the result directly.
+         */
+        apply(a: A, params: P, context: TransformContext): Task<B> | B,
+
+        /**
+         * Attempts to update the entity in a non-destructive way.
+         * For example changing a color scheme of a visual does not require computing new geometry.
+         * Return/resolve to undefined if the update is not possible.
+         *
+         * The ability to resolve the task to undefined is present for "async updates" (i.e. containing an ajax call).
+         */
+        update?(a: A, oldParams: P, b: B, newParams: P, context: TransformContext): Task<B | undefined> | B | undefined,
+
+        /** Check the parameters and return a list of errors if the are not valid. */
+        defaultParams?(a: A, context: TransformContext): P,
+
+        /** Specify default control descriptors for the parameters */
+        defaultControls?(a: A, context: TransformContext): Transformer.ControlsFor<P>,
+
+        /** Check the parameters and return a list of errors if the are not valid. */
+        validateParams?(a: A, params: P, context: TransformContext): string[] | undefined,
+
+        /** Optional custom parameter equality. Use deep structural equal by default. */
+        areParamsEqual?(oldParams: P, newParams: P): boolean,
+
+        /** Test if the transform can be applied to a given node */
+        isApplicable?(a: A, context: TransformContext): boolean,
+
+        /** By default, returns true */
+        isSerializable?(params: P): { isSerializable: true } | { isSerializable: false; reason: string },
+
+        /** Custom conversion to and from JSON */
+        customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P }
+    }
+
+    const registry = new Map<Id, Transformer>();
+
+    function typeToString(a: { type: StateObject.Type }[]) {
+        if (!a.length) return '()';
+        if (a.length === 1) return a[0].type.kind;
+        return `(${a.map(t => t.type.kind).join(' | ')})`;
+    }
+
+    export function get(id: string): Transformer {
+        const t = registry.get(id as Id);
+        if (!t) {
+            throw new Error(`A transformer with signature '${id}' is not registered.`);
+        }
+        return t;
+    }
+
+    export function create<A extends StateObject, B extends StateObject, P>(namespace: string, definition: Definition<A, B, P>) {
+        const { from, to, name } = definition;
+        const id = `${namespace}.${name} :: ${typeToString(from)} -> ${typeToString(to)}` as Id;
+
+        if (registry.has(id)) {
+            throw new Error(`A transform with id '${name}' is already registered. Please pick a unique identifier for your transforms and/or register them only once. This is to ensure that transforms can be serialized and replayed.`);
+        }
+
+        const t: Transformer<A, B, P> = {
+            apply(params, props) { return Transform.create<A, B, P>(t as any, params, props); },
+            id,
+            definition
+        };
+        registry.set(id, t);
+
+        return t;
+    }
+
+    export function factory(namespace: string) {
+        return <A extends StateObject, B extends StateObject, P>(definition: Definition<A, B, P>) => create(namespace, definition);
+    }
+
+    export const ROOT = create<any, any, any>('build-in', {
+        name: 'root',
+        from: [],
+        to: [],
+        apply() { throw new Error('should never be applied'); }
+    })
 }

+ 45 - 0
src/mol-state/tree/builder.ts

@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ImmutableTree } from '../util/immutable-tree';
+import { TransformTree } from './tree';
+import { StateObject } from '../object';
+import { Transform } from './transform';
+
+export interface StateTreeBuilder {
+    getTree(): TransformTree
+}
+
+export namespace StateTreeBuilder {
+    interface State {
+        tree: TransformTree.Transient
+    }
+
+    export function create(tree: TransformTree) {
+        return new Root(tree);
+    }
+
+    export class Root implements StateTreeBuilder {
+        private state: State;
+        to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref); }
+        toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.rootRef as any); }
+        getTree(): TransformTree { return this.state.tree.asImmutable(); }
+        constructor(tree: TransformTree) { this.state = { tree: ImmutableTree.asTransient(tree) } }
+    }
+
+    export class To<A extends StateObject> implements StateTreeBuilder {
+        apply<B extends StateObject>(t: Transform<A, B, any>): To<B> {
+            this.state.tree.add(this.ref, t);
+            return new To(this.state, t.ref);
+        }
+
+        getTree(): TransformTree { return this.state.tree.asImmutable(); }
+
+        constructor(private state: State, private ref: Transform.Ref) {
+
+        }
+    }
+}

+ 0 - 9
src/mol-state/tree/transation.ts

@@ -1,9 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-export interface TreeTransaction {
-    
-}

+ 57 - 10
src/mol-state/tree/transform.ts

@@ -6,28 +6,75 @@
 
 import { StateObject } from '../object';
 import { Transformer } from '../transformer';
+import { UUID } from 'mol-util';
 
-export interface Transform<A extends StateObject, B extends StateObject, P = any> {
-    readonly instanceId: Transform.InstanceId,
-
+export interface Transform<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
     readonly transformer: Transformer<A, B, P>,
-    readonly props: Transform.Props,
-
-    readonly transformerId: string,
     readonly params: P,
-    readonly ref: string
-    // version is part of the tree
+    readonly ref: Transform.Ref,
+    readonly version: string
 }
 
 export namespace Transform {
-    export type InstanceId = number & { '@type': 'transform-instance-id' }
+    export type Ref = string
 
     export interface Props {
-
+        ref: Ref
     }
 
     export enum Flags {
         // Indicates that the transform was generated by a behaviour and should not be automatically updated
         Generated
     }
+
+    export function create<A extends StateObject, B extends StateObject, P>(transformer: Transformer<A, B, P>, params?: P, props?: Partial<Props>): Transform<A, B, P> {
+        const ref = props && props.ref ? props.ref : UUID.create() as string as Ref;
+        return {
+            transformer,
+            params: params || { } as any,
+            ref,
+            version: UUID.create()
+        }
+    }
+
+    export function updateParams<T>(t: Transform, params: any): Transform {
+        return { ...t, params, version: UUID.create() };
+    }
+
+    export function createRoot(ref: Ref): Transform {
+        return create(Transformer.ROOT, {}, { ref });
+    }
+
+    export interface Serialized {
+        transformer: string,
+        params: any,
+        ref: string,
+        version: string
+    }
+
+    function _id(x: any) { return x; }
+    export function toJSON(t: Transform): Serialized {
+        const pToJson = t.transformer.definition.customSerialization
+            ? t.transformer.definition.customSerialization.toJSON
+            : _id;
+        return {
+            transformer: t.transformer.id,
+            params: pToJson(t.params),
+            ref: t.ref,
+            version: t.version
+        };
+    }
+
+    export function fromJSON(t: Serialized): Transform {
+        const transformer = Transformer.get(t.transformer);
+        const pFromJson = transformer.definition.customSerialization
+            ? transformer.definition.customSerialization.toJSON
+            : _id;
+        return {
+            transformer,
+            params: pFromJson(t.params),
+            ref: t.ref,
+            version: t.version
+        };
+    }
 }

+ 27 - 7
src/mol-state/tree/tree.ts

@@ -4,14 +4,34 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-export interface TransformTree {
-    // TODO
-}
+import { Transform } from './transform';
+import { ImmutableTree } from '../util/immutable-tree';
+import { Transformer } from '../transformer';
+
+export interface TransformTree extends ImmutableTree<Transform> { }
 
 export namespace TransformTree {
-    export interface Update {
-        readonly tree: TransformTree,
-        readonly rootId: number,
-        readonly params: unknown
+    export interface Transient extends ImmutableTree.Transient<Transform> { }
+
+    function _getRef(t: Transform) { return t.ref; }
+
+    export function create() {
+        return ImmutableTree.create<Transform>(Transform.createRoot('<:root:>'), _getRef);
+    }
+
+    export function updateParams<T extends Transformer = Transformer>(tree: TransformTree, ref: Transform.Ref, params: Transformer.Params<T>): TransformTree {
+        const t = tree.nodes.get(ref)!.value;
+        const newTransform = Transform.updateParams(t, params);
+        const newTree = ImmutableTree.asTransient(tree);
+        newTree.setValue(ref, newTransform);
+        return newTree.asImmutable();
+    }
+
+    export function toJSON(tree: TransformTree) {
+        return ImmutableTree.toJSON(tree, Transform.toJSON);
+    }
+
+    export function fromJSON(data: any): TransformTree {
+        return ImmutableTree.fromJSON(data, _getRef, Transform.fromJSON);
     }
 }

+ 81 - 23
src/mol-state/util/immutable-tree.ts

@@ -6,31 +6,36 @@
 
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
 
-// TODO: use generic "node keys" instead of string
-
 /**
  * An immutable tree where each node requires a unique reference.
  * Represented as an immutable map.
  */
 export interface ImmutableTree<T> {
-    readonly rootRef: string,
+    readonly rootRef: ImmutableTree.Ref,
     readonly version: number,
     readonly nodes: ImmutableTree.Nodes<T>,
-    getRef(e: T): string
+    getRef(e: T): ImmutableTree.Ref,
+    getValue(ref: ImmutableTree.Ref): T | undefined
 }
 
 export namespace ImmutableTree {
-    export interface MutableNode<T> { ref: string, value: T, version: number, parent: string, children: OrderedSet<string> }
+    export type Ref = string
+    export interface MutableNode<T> { ref: ImmutableTree.Ref, value: T, version: number, parent: ImmutableTree.Ref, children: OrderedSet<ImmutableTree.Ref> }
     export interface Node<T> extends Readonly<MutableNode<T>> { }
-    export interface Nodes<T> extends ImmutableMap<string, Node<T>> { }
+    export interface Nodes<T> extends ImmutableMap<ImmutableTree.Ref, Node<T>> { }
 
     class Impl<T> implements ImmutableTree<T> {
-        readonly rootRef: string;
+        readonly rootRef: ImmutableTree.Ref;
         readonly version: number;
         readonly nodes: ImmutableTree.Nodes<T>;
-        readonly getRef: (e: T) => string;
+        readonly getRef: (e: T) => ImmutableTree.Ref;
+
+        getValue(ref: Ref) {
+            const n = this.nodes.get(ref);
+            return n ? n.value : void 0;
+        }
 
-        constructor(rootRef: string, nodes: ImmutableTree.Nodes<T>, getRef: (e: T) => string, version: number) {
+        constructor(rootRef: ImmutableTree.Ref, nodes: ImmutableTree.Nodes<T>, getRef: (e: T) => ImmutableTree.Ref, version: number) {
             this.rootRef = rootRef;
             this.nodes = nodes;
             this.getRef = getRef;
@@ -41,7 +46,7 @@ export namespace ImmutableTree {
     /**
      * Create an instance of an immutable tree.
      */
-    export function create<T>(root: T, getRef: (t: T) => string): ImmutableTree<T> {
+    export function create<T>(root: T, getRef: (t: T) => ImmutableTree.Ref): ImmutableTree<T> {
         const ref = getRef(root);
         const r: Node<T> = { ref, value: root, version: 0, parent: ref, children: OrderedSet() };
         return new Impl(ref, ImmutableMap([[ref, r]]), getRef, 0);
@@ -56,7 +61,7 @@ export namespace ImmutableTree {
 
     type VisitorCtx = { nodes: Ns, state: any, f: (node: N, nodes: Ns, state: any) => boolean | undefined | void };
 
-    function _postOrderFunc(this: VisitorCtx, c: string | undefined) { _doPostOrder(this, this.nodes.get(c!)!); }
+    function _postOrderFunc(this: VisitorCtx, c: ImmutableTree.Ref | undefined) { _doPostOrder(this, this.nodes.get(c!)!); }
     function _doPostOrder<T, S>(ctx: VisitorCtx, root: N) {
         if (root.children.size) {
             root.children.forEach(_postOrderFunc, ctx);
@@ -73,9 +78,10 @@ export namespace ImmutableTree {
         return ctx.state;
     }
 
-    function _preOrderFunc(this: VisitorCtx, c: string | undefined) { _doPreOrder(this, this.nodes.get(c!)!); }
+    function _preOrderFunc(this: VisitorCtx, c: ImmutableTree.Ref | undefined) { _doPreOrder(this, this.nodes.get(c!)!); }
     function _doPreOrder<T, S>(ctx: VisitorCtx, root: N) {
-        ctx.f(root, ctx.nodes, ctx.state);
+        const ret = ctx.f(root, ctx.nodes, ctx.state);
+        if (typeof ret === 'boolean' && !ret) return;
         if (root.children.size) {
             root.children.forEach(_preOrderFunc, ctx);
         }
@@ -83,6 +89,7 @@ export 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<T, S>(tree: ImmutableTree<T>, root: Node<T>, state: S, f: (node: Node<T>, nodes: Nodes<T>, state: S) => boolean | undefined | void) {
         const ctx: VisitorCtx = { nodes: tree.nodes, state, f };
@@ -98,25 +105,71 @@ export namespace ImmutableTree {
         return doPostOrder<T, Node<T>[]>(tree, root, [], _subtree);
     }
 
-    function checkSetRef(oldRef: string, newRef: string) {
+
+    function _visitChildToJson(this: Ref[], ref: Ref) { this.push(ref); }
+    interface ToJsonCtx { nodes: Ref[], parent: any, children: any, values: any, valueToJSON: (v: any) => any }
+    function _visitNodeToJson(this: ToJsonCtx, node: Node<any>) {
+        this.nodes.push(node.ref);
+        const children: Ref[] = [];
+        node.children.forEach(_visitChildToJson as any, children);
+        this.parent[node.ref] = node.parent;
+        this.children[node.ref] = children;
+        this.values[node.ref] = this.valueToJSON(node.value);
+    }
+
+    export interface Serialized {
+        root: Ref,
+        nodes: Ref[],
+        parent: { [key: string]: string },
+        children: { [key: string]: any },
+        values: { [key: string]: any }
+    }
+
+    export function toJSON<T>(tree: ImmutableTree<T>, valueToJSON: (v: T) => any): Serialized {
+        const ctx: ToJsonCtx = { nodes: [], parent: { }, children: {}, values: {}, valueToJSON };
+        tree.nodes.forEach(_visitNodeToJson as any, ctx);
+        return {
+            root: tree.rootRef,
+            nodes: ctx.nodes,
+            parent: ctx.parent,
+            children: ctx.children,
+            values: ctx.values
+        };
+    }
+
+    export function fromJSON<T>(data: Serialized, getRef: (v: T) => Ref, valueFromJSON: (v: any) => T): ImmutableTree<T> {
+        const nodes = ImmutableMap<ImmutableTree.Ref, Node<T>>().asMutable();
+        for (const ref of data.nodes) {
+            nodes.set(ref, {
+                ref,
+                value: valueFromJSON(data.values[ref]),
+                version: 0,
+                parent: data.parent[ref],
+                children: OrderedSet(data.children[ref])
+            });
+        }
+        return new Impl(data.root, nodes.asImmutable(), getRef, 0);
+    }
+
+    function checkSetRef(oldRef: ImmutableTree.Ref, newRef: ImmutableTree.Ref) {
         if (oldRef !== newRef) {
             throw new Error(`Cannot setValue of node '${oldRef}' because the new value has a different ref '${newRef}'.`);
         }
     }
 
-    function ensureNotPresent(nodes: Ns, ref: string) {
+    function ensureNotPresent(nodes: Ns, 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 ensurePresent(nodes: Ns, ref: string) {
+    function ensurePresent(nodes: Ns, ref: ImmutableTree.Ref) {
         if (!nodes.has(ref)) {
             throw new Error(`Node '${ref}' is not present in the tree.`);
         }
     }
 
-    function mutateNode(nodes: Ns, mutations: Map<string, N>, ref: string): N {
+    function mutateNode(nodes: Ns, mutations: Map<ImmutableTree.Ref, N>, ref: ImmutableTree.Ref): N {
         ensurePresent(nodes, ref);
         if (mutations.has(ref)) {
             return mutations.get(ref)!;
@@ -131,9 +184,9 @@ export namespace ImmutableTree {
     export class Transient<T> implements ImmutableTree<T> {
         nodes = this.tree.nodes.asMutable();
         version: number = this.tree.version + 1;
-        private mutations: Map<string, Node<T>> = new Map();
+        private mutations: Map<ImmutableTree.Ref, Node<T>> = new Map();
 
-        mutate(ref: string): MutableNode<T> {
+        mutate(ref: ImmutableTree.Ref): MutableNode<T> {
             return mutateNode(this.nodes, this.mutations, ref);
         }
 
@@ -142,7 +195,12 @@ export namespace ImmutableTree {
             return this.tree.getRef(e);
         }
 
-        add(parentRef: string, value: T) {
+        getValue(ref: Ref) {
+            const n = this.nodes.get(ref);
+            return n ? n.value : void 0;
+        }
+
+        add(parentRef: ImmutableTree.Ref, value: T) {
             const ref = this.getRef(value);
             ensureNotPresent(this.nodes, ref);
             const parent = this.mutate(parentRef);
@@ -153,14 +211,14 @@ export namespace ImmutableTree {
             return node;
         }
 
-        setValue(ref: string, value: T): Node<T> {
+        setValue(ref: ImmutableTree.Ref, value: T): Node<T> {
             checkSetRef(ref, this.getRef(value));
             const node = this.mutate(ref);
             node.value = value;
             return node;
         }
 
-        remove<T>(ref: string): Node<T>[] {
+        remove<T>(ref: ImmutableTree.Ref): Node<T>[] {
             const { nodes, mutations, mutate } = this;
             const node = nodes.get(ref);
             if (!node) return [];
@@ -180,7 +238,7 @@ export namespace ImmutableTree {
             return st;
         }
 
-        removeChildren(ref: string): Node<T>[] {
+        removeChildren(ref: ImmutableTree.Ref): Node<T>[] {
             const { nodes, mutations, mutate } = this;
             let node = nodes.get(ref);
             if (!node || !node.children.size) return [];

+ 131 - 0
src/perf-tests/state.ts

@@ -0,0 +1,131 @@
+import { Transformer } from 'mol-state/transformer';
+import { StateObject } from 'mol-state/object';
+import { Task } from 'mol-task';
+import { TransformTree } from 'mol-state/tree/tree';
+import { StateTreeBuilder } from 'mol-state/tree/builder';
+import { State } from 'mol-state/state';
+import * as util from 'util'
+
+export type TypeClass = 'root' | 'shape' | 'prop'
+export interface ObjProps { label: string }
+export interface TypeInfo { name: string, class: TypeClass }
+
+const _obj = StateObject.factory<TypeInfo, ObjProps>()
+const _transform = Transformer.factory('test');
+
+export class Root extends _obj('root', { name: 'Root', class: 'root' }) { }
+export class Square extends _obj<{ a: number }>('square', { name: 'Square', class: 'shape' }) { }
+export class Circle extends _obj<{ r: number }>('circle', { name: 'Circle', class: 'shape' }) { }
+export class Area extends _obj<{ volume: number }>('volume', { name: 'Volume', class: 'prop' }) { }
+
+const root = new Root({ label: 'Root' }, {});
+
+export const CreateSquare = _transform<Root, Square, { a: number }>({
+    name: 'create-square',
+    from: [Root],
+    to: [Square],
+    apply(a, p) {
+        return new Square({ label: `Square a=${p.a}` }, p);
+    },
+    update(a, _, b, p) {
+        b.props.label = `Square a=${p.a}`
+        b.data.a = p.a;
+        return b;
+    }
+});
+
+export const CreateCircle = _transform<Root, Circle, { r: number }>({
+    name: 'create-circle',
+    from: [Root],
+    to: [Square],
+    apply(a, p) {
+        return new Circle({ label: `Circle r=${p.r}` }, p);
+    },
+    update(a, _, b, p) {
+        b.props.label = `Circle r=${p.r}`
+        b.data.r = p.r;
+        return b;
+    }
+});
+
+export const CaclArea = _transform<Square | Circle, Area, {}>({
+    name: 'calc-area',
+    from: [Square, Circle],
+    to: [Area],
+    apply(a) {
+        if (a instanceof Square) return new Area({ label: 'Area' }, { volume: a.data.a * a.data.a });
+        else if (a instanceof Circle) return new Area({ label: 'Area' }, { volume: a.data.r * a.data.r * Math.PI });
+        throw new Error('Unknown object type.');
+    },
+    update(a, _, b) {
+        if (a instanceof Square) b.data.volume = a.data.a * a.data.a;
+        else if (a instanceof Circle) b.data.volume = a.data.r * a.data.r * Math.PI;
+        else throw new Error('Unknown object type.');
+        return b;
+    }
+});
+
+async function runTask<A>(t: A | Task<A>): Promise<A> {
+    if ((t as any).run) return await (t as Task<A>).run();
+    return t as A;
+}
+
+export async function test() {
+    const sq = await runTask(CreateSquare.definition.apply(root, { a: 10 }, 0 as any));
+    const area = await runTask(CaclArea.definition.apply(sq, {}, 0 as any));
+    console.log(area);
+}
+
+export async function testState() {
+    const state = State.create();
+
+    const tree = state.definition.tree;
+    const builder = StateTreeBuilder.create(tree)
+    builder.toRoot<Root>()
+        .apply(CreateSquare.apply({ a: 10 }, { ref: 'square' }))
+        .apply(CaclArea.apply());
+    const tree1 = builder.getTree();
+
+    printTTree(tree1);
+
+    const tree2 = TransformTree.updateParams<typeof CreateSquare>(tree1, 'square', { a: 15 });
+    printTTree(tree1);
+    printTTree(tree2);
+
+    const state1 = await State.update(state, tree1);
+    console.log('----------------');
+    console.log(util.inspect(state1.objects, true, 3, true));
+
+    console.log('----------------');
+    const jsonString = JSON.stringify(TransformTree.toJSON(tree2), null, 2);
+    const jsonData = JSON.parse(jsonString);
+    printTTree(tree2);
+    console.log(jsonString);
+    const treeFromJson = TransformTree.fromJSON(jsonData);
+    printTTree(treeFromJson);
+
+    console.log('----------------');
+    const state2 = await State.update(state1, treeFromJson);
+    console.log(util.inspect(state2.objects, true, 3, true));
+}
+
+testState();
+
+
+//test();
+
+export function printTTree(tree: TransformTree) {
+    let lines: string[] = [];
+    function print(offset: string, ref: any) {
+        let t = tree.nodes.get(ref)!;
+        let tr = t.value;
+
+        const name = tr.transformer.id;
+        lines.push(`${offset}|_ (${ref}) ${name} ${tr.params ? JSON.stringify(tr.params) : ''}, v${t.value.version}`);
+        offset += '   ';
+
+        t.children.forEach(c => print(offset, c!));
+    }
+    print('', tree.rootRef);
+    console.log(lines.join('\n'));
+}