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

mol-state: wip

parent 3a22246e
No related branches found
No related tags found
No related merge requests found
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
...@@ -116,6 +116,7 @@ ...@@ -116,6 +116,7 @@
"graphql": "^14.0.2", "graphql": "^14.0.2",
"graphql-request": "^1.8.2", "graphql-request": "^1.8.2",
"immer": "^1.7.1", "immer": "^1.7.1",
"immutable": "^3.8.2",
"node-fetch": "^2.2.0", "node-fetch": "^2.2.0",
"react": "^16.5.2", "react": "^16.5.2",
"react-dom": "^16.5.2", "react-dom": "^16.5.2",
......
/**
* 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
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
*/ */
import { TransformTree } from '../transform/tree'; import { TransformTree } from '../tree/tree';
import { ModelTree } from './tree'; import { ModelTree } from './tree';
export function reconcileTree(transform: TransformTree, model: ModelTree, root?: number) { export function reconcileTree(transform: TransformTree, model: ModelTree, root?: number) {
......
/**
* 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
...@@ -4,14 +4,12 @@ ...@@ -4,14 +4,12 @@
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
*/ */
export interface ModelNode<T = any> { import { EventDispatcher } from '../context/event';
'@type': T
}
export namespace ModelNode { export interface TransformContext {
export type TypeOf<T> /** An event dispatcher for executing child tasks. */
= T extends ModelNode<infer X> ? [X] dispatcher: EventDispatcher,
: T extends [ModelNode<infer X>] ? [X]
: T extends [ModelNode<infer X>, ModelNode<infer Y>] ? [X, Y] globalContext: any
: unknown[]; // tree: ModelTree
} }
\ 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>
*/
// TODO
\ No newline at end of file
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
*/ */
import { Transform } from './transform'; import { Transform } from './transform';
import { ModelNode } from '../model/node'; import { StateObject } from '../model/object';
import { Transformer } from './transformer'; 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 instanceId: number,
readonly transformer: Transformer<A, B, P>, readonly transformer: Transformer<A, B, P>,
......
/**
* 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
File moved
/**
* 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
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