From b358c3baab38497ef196aa22881ed0a5ef2d94cb Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Wed, 17 Oct 2018 13:57:00 +0200 Subject: [PATCH] mol-state: wip --- package-lock.json | Bin 412711 -> 412882 bytes package.json | 1 + src/mol-state/model/node.ts | 17 -- src/mol-state/model/object.ts | 29 +++ src/mol-state/model/reconcile.ts | 2 +- src/mol-state/transform/transformer.ts | 69 ------ src/mol-state/tree/context.ts | 15 ++ src/mol-state/tree/selection.ts | 7 + .../{transform => tree}/transform.ts | 4 +- src/mol-state/tree/transformer.ts | 57 +++++ src/mol-state/{transform => tree}/tree.ts | 0 src/mol-state/util/immutable-tree.ts | 207 ++++++++++++++++++ 12 files changed, 319 insertions(+), 89 deletions(-) delete mode 100644 src/mol-state/model/node.ts create mode 100644 src/mol-state/model/object.ts delete mode 100644 src/mol-state/transform/transformer.ts create mode 100644 src/mol-state/tree/context.ts create mode 100644 src/mol-state/tree/selection.ts rename src/mol-state/{transform => tree}/transform.ts (80%) create mode 100644 src/mol-state/tree/transformer.ts rename src/mol-state/{transform => tree}/tree.ts (100%) create mode 100644 src/mol-state/util/immutable-tree.ts diff --git a/package-lock.json b/package-lock.json index 25dccbec517c83159a708ec29ec305d1a34fc3ef..3e47f1bf9a0e5a25ff2e327f46e4c1db1c39d7a2 100644 GIT binary patch delta 357 zcmZ2JL-Nv0$qj~krwiO=7Mp(Fh)rqxIyn|Grp*oetxF(+1#_5Lr&rHr7Tx~Fn6b_b zBIujV$g#aMi}4^6lFsH_#_zgd{`QHp8DCC?NC_w~v234e$aIfs`h+gliPLT|a&H$f zWy+0!NSo@i$Zg-$%XE^1CAYLBar!|~Mkz+)=>ansO~o^FK|)D6srtJ5aJH`T^nx?Y zD$?cI{!yW!VM)cN*&#+{-cEi-7Gb{ruC7ICzT2~=F)>{RS+{Jva2B(p1c<4%y}pUr zg9F4A-+sS~`JLDF1_3t7?N6^VPw+tU1<?Oi%@AMwU&xZ0f-0rSIQ_vFR<-SK*jZ)e fAU14JV{QHgQRT11#=bpJku6djV!%NQwlrY?m%)NZ delta 268 zcmcaKQ*!wX$qj~krx~(HO;5;WRNS5*&mzJ!y&;xKbn}$`Y9$j7h;P4O%s9gg$e32Z z$hN&Di}5fMNcnc4axuN>6KYr_xA)Cvd_HxW8k6+)enX~vOw%u<GfPaLH;1Kay1;ct z?(GV?O!6$-H7uF@LZ&Y$XOh_7+spKfW4qpTrjM84YWuez&0=<xfb)g78#FU}aDe&S z`@5Onc}<>hN^JX-Ys`~8P@HSgJnb~I#P;=zSW;3@B-9zFC;Vns+dhkfRaOpS&N($! bq2JR#$g#<7|EtIrDGp*vY|pi1OBDtH%a>z3 diff --git a/package.json b/package.json index 560a89853..46e2beefe 100644 --- a/package.json +++ b/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", diff --git a/src/mol-state/model/node.ts b/src/mol-state/model/node.ts deleted file mode 100644 index cd4807c60..000000000 --- a/src/mol-state/model/node.ts +++ /dev/null @@ -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[]; -} \ No newline at end of file diff --git a/src/mol-state/model/object.ts b/src/mol-state/model/object.ts new file mode 100644 index 000000000..9665765e4 --- /dev/null +++ b/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 + } +} \ No newline at end of file diff --git a/src/mol-state/model/reconcile.ts b/src/mol-state/model/reconcile.ts index 95997d7ce..16b439438 100644 --- a/src/mol-state/model/reconcile.ts +++ b/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) { diff --git a/src/mol-state/transform/transformer.ts b/src/mol-state/transform/transformer.ts deleted file mode 100644 index 271710542..000000000 --- a/src/mol-state/transform/transformer.ts +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/src/mol-state/tree/context.ts b/src/mol-state/tree/context.ts new file mode 100644 index 000000000..8c4bb7c09 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/src/mol-state/tree/selection.ts b/src/mol-state/tree/selection.ts new file mode 100644 index 000000000..7c7fe8338 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/src/mol-state/transform/transform.ts b/src/mol-state/tree/transform.ts similarity index 80% rename from src/mol-state/transform/transform.ts rename to src/mol-state/tree/transform.ts index 7234fd6fc..fd3569665 100644 --- a/src/mol-state/transform/transform.ts +++ b/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>, diff --git a/src/mol-state/tree/transformer.ts b/src/mol-state/tree/transformer.ts new file mode 100644 index 000000000..29d1f97c7 --- /dev/null +++ b/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 } +} \ No newline at end of file diff --git a/src/mol-state/transform/tree.ts b/src/mol-state/tree/tree.ts similarity index 100% rename from src/mol-state/transform/tree.ts rename to src/mol-state/tree/tree.ts diff --git a/src/mol-state/util/immutable-tree.ts b/src/mol-state/util/immutable-tree.ts new file mode 100644 index 000000000..e0d04147d --- /dev/null +++ b/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>) { + + } + } +} \ No newline at end of file -- GitLab