David Sehnal 6 éve
szülő
commit
b358c3baab

+ 23 - 18
package-lock.json

@@ -1254,7 +1254,7 @@
         },
         "readable-stream": {
           "version": "1.0.34",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
           "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
           "dev": true,
           "requires": {
@@ -2114,7 +2114,7 @@
       "dependencies": {
         "node-fetch": {
           "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
+          "resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
           "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U="
         }
       }
@@ -2501,7 +2501,7 @@
     },
     "duplexer": {
       "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
+      "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
       "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
       "dev": true
     },
@@ -2522,7 +2522,7 @@
         },
         "readable-stream": {
           "version": "1.1.14",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+          "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
           "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
           "dev": true,
           "requires": {
@@ -2863,7 +2863,7 @@
     },
     "express": {
       "version": "4.16.3",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
+      "resolved": "http://registry.npmjs.org/express/-/express-4.16.3.tgz",
       "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
       "requires": {
         "accepts": "~1.3.5",
@@ -4124,7 +4124,7 @@
     },
     "glslify": {
       "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz",
+      "resolved": "http://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz",
       "integrity": "sha1-R6jOW/CGCVVqp+x2xqfTQwd23UY=",
       "dev": true,
       "requires": {
@@ -4147,7 +4147,7 @@
         },
         "minimist": {
           "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
         },
@@ -4761,7 +4761,7 @@
     },
     "http-errors": {
       "version": "1.6.3",
-      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+      "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
       "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
       "requires": {
         "depd": "~1.1.2",
@@ -4824,6 +4824,11 @@
       "resolved": "https://registry.npmjs.org/immer/-/immer-1.7.1.tgz",
       "integrity": "sha512-EhaCOnzhNazyzfWVKVUIQwcNwKMK+bpyBRsFajul8eivaloB/JtCjSoZwUR9Ml/+ZhuPXufnp7QYvvSJ2ye/Eg=="
     },
+    "immutable": {
+      "version": "3.8.2",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
+      "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
+    },
     "import-local": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz",
@@ -6349,7 +6354,7 @@
       "dependencies": {
         "minimist": {
           "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
         }
@@ -6515,7 +6520,7 @@
     },
     "minimist": {
       "version": "0.0.8",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
       "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
       "dev": true
     },
@@ -6578,7 +6583,7 @@
     },
     "mkdirp": {
       "version": "0.5.1",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
       "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
       "dev": true,
       "requires": {
@@ -7137,7 +7142,7 @@
     },
     "p-is-promise": {
       "version": "1.1.0",
-      "resolved": "http://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz",
+      "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz",
       "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=",
       "dev": true
     },
@@ -7660,7 +7665,7 @@
         },
         "readable-stream": {
           "version": "1.0.34",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
           "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
           "dev": true,
           "requires": {
@@ -8582,7 +8587,7 @@
         },
         "os-locale": {
           "version": "1.4.0",
-          "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+          "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
           "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
           "dev": true,
           "requires": {
@@ -9294,7 +9299,7 @@
         },
         "readable-stream": {
           "version": "1.0.34",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
           "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
           "dev": true,
           "requires": {
@@ -9523,7 +9528,7 @@
       "dependencies": {
         "minimist": {
           "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
         }
@@ -11250,7 +11255,7 @@
     },
     "whatwg-fetch": {
       "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
+      "resolved": "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
       "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng=="
     },
     "whatwg-mimetype": {
@@ -11402,7 +11407,7 @@
     },
     "wrap-ansi": {
       "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+      "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
       "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
       "dev": true,
       "requires": {

+ 1 - 0
package.json

@@ -116,6 +116,7 @@
     "graphql": "^14.0.2",
     "graphql-request": "^1.8.2",
     "immer": "^1.7.1",
+    "immutable": "^3.8.2",
     "node-fetch": "^2.2.0",
     "react": "^16.5.2",
     "react-dom": "^16.5.2",

+ 0 - 17
src/mol-state/model/node.ts

@@ -1,17 +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 ModelNode<T = any> {
-    '@type': T
-}
-
-export namespace ModelNode {
-    export type TypeOf<T>
-        = T extends ModelNode<infer X> ? [X]
-        : T extends [ModelNode<infer X>] ? [X]
-        : T extends [ModelNode<infer X>, ModelNode<infer Y>] ? [X, Y]
-        : unknown[];
-}

+ 29 - 0
src/mol-state/model/object.ts

@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+export interface StateObject<T = any> {
+    '@type': T,
+    readonly label: string
+}
+
+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 enum StateType {
+        // The object has been successfully created
+        Ok,
+        // An error occured during the creation of the object
+        Error,
+        // The object is queued to be created
+        Pending,
+        // The object is currently being created
+        Processing
+    }
+}

+ 1 - 1
src/mol-state/model/reconcile.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { TransformTree } from '../transform/tree';
+import { TransformTree } from '../tree/tree';
 import { ModelTree } from './tree';
 
 export function reconcileTree(transform: TransformTree, model: ModelTree, root?: number) {

+ 0 - 69
src/mol-state/transform/transformer.ts

@@ -1,69 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { Task } from 'mol-task';
-import { EventDispatcher } from '../context/event';
-import { ModelNode } from '../model/node';
-import { ModelTree } from '../model/tree';
-
-export interface Transformer<A extends ModelNode, B extends ModelNode, P = any> {
-    readonly id: Transformer.Id,
-    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 interface Definition<A extends ModelNode, B extends ModelNode, P> {
-        readonly name: string,
-        readonly namespace: string,
-        readonly description?: string,
-        readonly from: ModelNode.TypeOf<A>[],
-        readonly to: ModelNode.TypeOf<B>[]
-
-        /**
-         * 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,
-
-        /**  */
-        defaultControls?(a: A, context: TransformContext): 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,
-
-        /** 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 },
-    }
-
-    export type ControlsFor<Props> = { [P in keyof Props]: any }
-
-    /** A tree context constructed dynamically duing application of transforms. */
-    export interface TransformContext {
-        /** An event dispatcher for executing child tasks. */
-        dispatcher: EventDispatcher,
-
-        globalContext: any,
-        tree: ModelTree
-    }
-}

+ 15 - 0
src/mol-state/tree/context.ts

@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { EventDispatcher } from '../context/event';
+
+export interface TransformContext {
+    /** An event dispatcher for executing child tasks. */
+    dispatcher: EventDispatcher,
+
+    globalContext: any
+    // tree: ModelTree
+}

+ 7 - 0
src/mol-state/tree/selection.ts

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

+ 2 - 2
src/mol-state/transform/transform.ts → src/mol-state/tree/transform.ts

@@ -5,10 +5,10 @@
  */
 
 import { Transform } from './transform';
-import { ModelNode } from '../model/node';
+import { StateObject } from '../model/object';
 import { Transformer } from './transformer';
 
-export interface Transform<A extends ModelNode, B extends ModelNode, P = any> {
+export interface Transform<A extends StateObject, B extends StateObject, P = any> {
     readonly instanceId: number,
 
     readonly transformer: Transformer<A, B, P>,

+ 57 - 0
src/mol-state/tree/transformer.ts

@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Task } from 'mol-task';
+import { StateObject } from '../model/object';
+import { TransformContext } from './context';
+
+export interface Transformer<A extends StateObject, B extends StateObject, P = any> {
+    readonly id: Transformer.Id,
+    readonly name: string,
+    readonly namespace: string,
+    readonly description?: string,
+    readonly from: StateObject.TypeOf<A>[],
+    readonly to: StateObject.TypeOf<B>[],
+
+    /**
+     * 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,
+
+    /** 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 }
+}
+
+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 }
+}

+ 0 - 0
src/mol-state/transform/tree.ts → src/mol-state/tree/tree.ts


+ 207 - 0
src/mol-state/util/immutable-tree.ts

@@ -0,0 +1,207 @@
+/**
+ * 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';
+
+/**
+ * An immutable tree where each node requires a unique reference.
+ * Represented as an immutable map.
+ */
+export interface ImmutableTree<T> {
+    readonly rootRef: string,
+    readonly version: number,
+    readonly nodes: ImmutableTree.Nodes<T>,
+    getRef(e: T): string
+}
+
+export namespace ImmutableTree {
+    export interface MutableNode<T> { ref: string, value: T, version: number, parent: string, children: OrderedSet<string> }
+    export interface Node<T> extends Readonly<MutableNode<T>> { }
+    export interface Nodes<T> extends ImmutableMap<string, Node<T>> { }
+
+    class Impl<T> implements ImmutableTree<T> {
+        readonly rootRef: string;
+        readonly version: number;
+        readonly nodes: ImmutableTree.Nodes<T>;
+        readonly getRef: (e: T) => string;
+
+        constructor(rootRef: string, nodes: ImmutableTree.Nodes<T>, getRef: (e: T) => string, version: number) {
+            this.rootRef = rootRef;
+            this.nodes = nodes;
+            this.getRef = getRef;
+            this.version = version;
+        }
+    }
+
+    /**
+     * Create an instance of an immutable tree.
+     */
+    export function create<T>(root: T, getRef: (t: T) => string): 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);
+    }
+
+    export function asTransient<T>(tree: ImmutableTree<T>) {
+        return new Transient(tree);
+    }
+
+    type N = Node<any>
+    type Ns = Nodes<any>
+
+    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 _doPostOrder<T, S>(ctx: VisitorCtx, root: N) {
+        if (root.children.size) {
+            root.children.forEach(_postOrderFunc, ctx);
+        }
+        ctx.f(root, ctx.nodes, ctx.state);
+    }
+
+    /**
+     * Visit all nodes in a subtree in "post order", meaning leafs get visited first.
+     */
+    export function doPostOrder<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 };
+        _doPostOrder(ctx, root);
+        return ctx.state;
+    }
+
+    function _preOrderFunc(this: VisitorCtx, c: string | undefined) { _doPreOrder(this, this.nodes.get(c!)!); }
+    function _doPreOrder<T, S>(ctx: VisitorCtx, root: N) {
+        ctx.f(root, ctx.nodes, ctx.state);
+        if (root.children.size) {
+            root.children.forEach(_preOrderFunc, ctx);
+        }
+    }
+
+    /**
+     * Visit all nodes in a subtree in "pre order", meaning leafs get visited last.
+     */
+    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 };
+        _doPreOrder(ctx, root);
+        return ctx.state;
+    }
+
+    function _subtree(n: N, nodes: Ns, subtree: N[]) { subtree.push(n); }
+    /**
+     * Get all nodes in a subtree, leafs come first.
+     */
+    export function subtreePostOrder<T>(tree: ImmutableTree<T>, root: Node<T>) {
+        return doPostOrder<T, Node<T>[]>(tree, root, [], _subtree);
+    }
+
+    function checkSetRef(oldRef: string, newRef: string) {
+        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) {
+        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) {
+        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 {
+        ensurePresent(nodes, ref);
+        if (mutations.has(ref)) {
+            return mutations.get(ref)!;
+        }
+        const node = nodes.get(ref)!;
+        const newNode: N = { ref: node.ref, value: node.value, version: node.version + 1, parent: node.parent, children: node.children.asMutable() };
+        mutations.set(ref, newNode);
+        nodes.set(ref, newNode);
+        return newNode;
+    }
+
+    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();
+
+        mutate(ref: string): MutableNode<T> {
+            return mutateNode(this.nodes, this.mutations, ref);
+        }
+
+        get rootRef() { return this.tree.rootRef; }
+        getRef(e: T) {
+            return this.tree.getRef(e);
+        }
+
+        add(parentRef: string, value: T) {
+            const ref = this.getRef(value);
+            ensureNotPresent(this.nodes, ref);
+            const parent = this.mutate(parentRef);
+            const node: Node<T> = { ref, version: 0, value, parent: parent.ref, children: OrderedSet<string>().asMutable() };
+            this.mutations.set(ref, node);
+            parent.children.add(ref);
+            this.nodes.set(ref, node);
+            return node;
+        }
+
+        setValue(ref: string, 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>[] {
+            const { nodes, mutations, mutate } = this;
+            const node = nodes.get(ref);
+            if (!node) return [];
+            const parent = nodes.get(node.parent)!;
+            const children = mutate(parent.ref).children;
+            const st = subtreePostOrder(this, node);
+            if (parent.ref === node.ref) {
+                nodes.clear();
+                mutations.clear();
+                return st;
+            }
+            children.delete(ref);
+            for (const n of st) {
+                nodes.delete(n.value.ref);
+                mutations.delete(n.value.ref);
+            }
+            return st;
+        }
+
+        removeChildren(ref: string): Node<T>[] {
+            const { nodes, mutations, mutate } = this;
+            let node = nodes.get(ref);
+            if (!node || !node.children.size) return [];
+            node = mutate(ref);
+            const st = subtreePostOrder(this, node);
+            node.children.clear();
+            for (const n of st) {
+                if (n === node) continue;
+                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 MutableNode<T>).children = m.children.asImmutable());
+            return new Impl<T>(this.tree.rootRef, this.nodes.asImmutable(), this.tree.getRef, this.version);
+        }
+
+        constructor(private tree: ImmutableTree<T>) {
+
+        }
+    }
+}