Skip to content
Snippets Groups Projects
Commit f6ef6427 authored by David Sehnal's avatar David Sehnal
Browse files

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

parent 2c252ac7
No related branches found
No related tags found
No related merge requests found
/**
* 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
......@@ -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);
}
}
......@@ -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
/**
* 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
/**
* 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
......@@ -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
......@@ -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
......@@ -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 [];
......
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment