diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts index 391f1e1d00fdc7f4e6a1c4aea6813adfe30793ce..38389e2a36c68f7c0404ae3609d85d1248b46122 100644 --- a/src/mol-state/object.ts +++ b/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 + } } \ No newline at end of file diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index fb65ca6dc04016c58c050c3c159531732256aaa6..6f3fa1c456c776f6a7aa9324e802f0967e876994 100644 --- a/src/mol-state/state.ts +++ b/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); } } diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index e621fcf494c1296eea0051a17c1e78481ac9703a..a4a824c56f41dec056b0f107942115254525874e 100644 --- a/src/mol-state/transformer.ts +++ b/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'); } + }) } \ 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 0000000000000000000000000000000000000000..4d1a70dc3c7e345335a7cec307426862ce7e1c42 --- /dev/null +++ b/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) { + + } + } +} \ No newline at end of file diff --git a/src/mol-state/tree/transation.ts b/src/mol-state/tree/transation.ts deleted file mode 100644 index e05c32a208b6852f79418070f2052173adc4c847..0000000000000000000000000000000000000000 --- a/src/mol-state/tree/transation.ts +++ /dev/null @@ -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 { - -} \ No newline at end of file diff --git a/src/mol-state/tree/transform.ts b/src/mol-state/tree/transform.ts index e0ea9dd2ac5c0c78dd42f8683e82d1e25386283e..7c7af9e93cf2bbc3bc546648a08a5125a0ef6e6e 100644 --- a/src/mol-state/tree/transform.ts +++ b/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 + }; + } } \ No newline at end of file diff --git a/src/mol-state/tree/tree.ts b/src/mol-state/tree/tree.ts index ee9863182de55f06a85876fb6e2860c6b1eea213..686a0941d23da77f3b6dc9f9225bb534a9dce152 100644 --- a/src/mol-state/tree/tree.ts +++ b/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); } } \ No newline at end of file diff --git a/src/mol-state/util/immutable-tree.ts b/src/mol-state/util/immutable-tree.ts index d0ca43c4f8b9c879b4c1cf124ed7a357747121fe..e4ab973da5e7fcc606aeef1fcec84d66fc975d3b 100644 --- a/src/mol-state/util/immutable-tree.ts +++ b/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 []; diff --git a/src/perf-tests/state.ts b/src/perf-tests/state.ts new file mode 100644 index 0000000000000000000000000000000000000000..72225c541f519c41e0a5c01fe2774752449bb41a --- /dev/null +++ b/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')); +} \ No newline at end of file