Skip to content
Snippets Groups Projects
state.ts 9.74 KiB
Newer Older
David Sehnal's avatar
David Sehnal committed
/**
 * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
 *
 * @author David Sehnal <david.sehnal@gmail.com>
 */

import { StateObject } from './object';
David Sehnal's avatar
David Sehnal committed
import { StateTree } from './tree';
David Sehnal's avatar
David Sehnal committed
import { Transform } from './tree/transform';
import { ImmutableTree } from './util/immutable-tree';
import { Transformer } from './transformer';
David Sehnal's avatar
David Sehnal committed
import { StateContext } from './context';
import { UUID } from 'mol-util';
David Sehnal's avatar
David Sehnal committed
import { RuntimeContext, Task } from 'mol-task';
David Sehnal's avatar
David Sehnal committed

David Sehnal's avatar
David Sehnal committed
export interface State {
    tree: StateTree,
David Sehnal's avatar
David Sehnal committed
    objects: State.Objects,
    context: StateContext
David Sehnal's avatar
David Sehnal committed
}

export namespace State {
David Sehnal's avatar
David Sehnal committed
    export type Ref = Transform.Ref
David Sehnal's avatar
David Sehnal committed
    export type Objects = Map<Ref, StateObject.Node>
David Sehnal's avatar
David Sehnal committed

David Sehnal's avatar
David Sehnal committed
    export function create(params?: { globalContext?: unknown, defaultObjectProps: unknown }) {
David Sehnal's avatar
David Sehnal committed
        const tree = StateTree.create();
        const objects: Objects = new Map();
        const root = tree.getValue(tree.rootRef)!;
David Sehnal's avatar
David Sehnal committed
        const defaultObjectProps = (params && params.defaultObjectProps) || { }
David Sehnal's avatar
David Sehnal committed
        objects.set(tree.rootRef, { obj: void 0 as any, state: StateObject.StateType.Ok, version: root.version, props: { ...defaultObjectProps } });
David Sehnal's avatar
David Sehnal committed
            tree,
David Sehnal's avatar
David Sehnal committed
            objects,
David Sehnal's avatar
David Sehnal committed
            context: StateContext.create({
                globalContext: params && params.globalContext,
                defaultObjectProps
            })
David Sehnal's avatar
David Sehnal committed
    export function update(state: State, tree: StateTree): Task<State> {
        return Task.create('Update Tree', taskCtx => {
            const ctx: UpdateContext = {
                stateCtx: state.context,
                taskCtx,
                oldTree: state.tree,
                tree: tree,
                objects: state.objects
            };
            return _update(ctx);
        })
    }
David Sehnal's avatar
David Sehnal committed

David Sehnal's avatar
David Sehnal committed
    async function _update(ctx: UpdateContext): Promise<State> {
        const roots = findUpdateRoots(ctx.objects, ctx.tree);
David Sehnal's avatar
David Sehnal committed
        const deletes = findDeletes(ctx);
        for (const d of deletes) {
David Sehnal's avatar
David Sehnal committed
            ctx.objects.delete(d);
            ctx.stateCtx.events.object.removed.next({ ref: d });
David Sehnal's avatar
David Sehnal committed
        initObjectState(ctx, roots);

        for (const root of roots) {
David Sehnal's avatar
David Sehnal committed
            await updateSubtree(ctx, root);
David Sehnal's avatar
David Sehnal committed
            tree: ctx.tree,
            objects: ctx.objects,
            context: ctx.stateCtx
David Sehnal's avatar
David Sehnal committed
    interface UpdateContext {
        stateCtx: StateContext,
David Sehnal's avatar
David Sehnal committed
        taskCtx: RuntimeContext,
        oldTree: StateTree,
David Sehnal's avatar
David Sehnal committed
        tree: StateTree,
        objects: Objects
    }

    function findUpdateRoots(objects: Objects, tree: StateTree) {
David Sehnal's avatar
David Sehnal committed
            roots: [] as Ref[],
            objects
        };

        ImmutableTree.doPreOrder(tree, tree.nodes.get(tree.rootRef)!, findState, (n, _, s) => {
            if (!s.objects.has(n.ref)) {
                s.roots.push(n.ref);
                return false;
            }
            const o = s.objects.get(n.ref)!;
            if (o.version !== n.value.version) {
                s.roots.push(n.ref);
                return false;
            }

            return true;
        });

        return findState.roots;
    }

David Sehnal's avatar
David Sehnal committed
    function findDeletes(ctx: UpdateContext): Ref[] {
        // TODO: do this in some sort of "tree order"?
        const deletes: Ref[] = [];
        const keys = ctx.objects.keys();
        while (true) {
            const key = keys.next();
            if (key.done) break;
            if (!ctx.tree.nodes.has(key.value)) deletes.push(key.value);
        }
        return deletes;
    }

    function setObjectState(ctx: UpdateContext, ref: Ref, state: StateObject.StateType, errorText?: string) {
David Sehnal's avatar
David Sehnal committed
        let changed = false;
David Sehnal's avatar
David Sehnal committed
        if (ctx.objects.has(ref)) {
            const obj = ctx.objects.get(ref)!;
David Sehnal's avatar
David Sehnal committed
            changed = obj.state !== state;
David Sehnal's avatar
David Sehnal committed
            obj.state = state;
            obj.errorText = errorText;
        } else {
David Sehnal's avatar
David Sehnal committed
            const obj = { state, version: UUID.create(), errorText, props: { ...ctx.stateCtx.defaultObjectProps } };
David Sehnal's avatar
David Sehnal committed
            ctx.objects.set(ref, obj);
David Sehnal's avatar
David Sehnal committed
            changed = true;
David Sehnal's avatar
David Sehnal committed
        }
David Sehnal's avatar
David Sehnal committed
        if (changed) ctx.stateCtx.events.object.stateChanged.next({ ref });
David Sehnal's avatar
David Sehnal committed
    function _initVisitor(t: ImmutableTree.Node<Transform>, _: any, ctx: UpdateContext) {
        setObjectState(ctx, t.ref, StateObject.StateType.Pending);
    }
    /** Return "resolve set" */
    function initObjectState(ctx: UpdateContext, roots: Ref[]) {
        for (const root of roots) {
            ImmutableTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, _initVisitor);
        }
    }

    function doError(ctx: UpdateContext, ref: Ref, errorText: string) {
        setObjectState(ctx, ref, StateObject.StateType.Error, errorText);
        const wrap = ctx.objects.get(ref)!;
        if (wrap.obj) {
            ctx.stateCtx.events.object.removed.next({ ref });
            wrap.obj = void 0;
        }

        const children = ctx.tree.nodes.get(ref)!.children.values();
        while (true) {
            const next = children.next();
            if (next.done) return;
            doError(ctx, next.value, 'Parent node contains error.');
        }
    }

    function findParent(tree: StateTree, objects: Objects, root: Ref, types: { type: StateObject.Type }[]): StateObject {
        let current = tree.nodes.get(root)!;
        while (true) {
            current = tree.nodes.get(current.parent)!;
David Sehnal's avatar
David Sehnal committed
            if (current.ref === tree.rootRef) {
                return objects.get(tree.rootRef)!.obj!;
            }
            const obj = objects.get(current.ref)!.obj!;
            for (const t of types) if (obj.type === t.type) return objects.get(current.ref)!.obj!;
David Sehnal's avatar
David Sehnal committed
    async function updateSubtree(ctx: UpdateContext, root: Ref) {
David Sehnal's avatar
David Sehnal committed
        setObjectState(ctx, root, StateObject.StateType.Processing);
David Sehnal's avatar
David Sehnal committed

        try {
            const update = await updateNode(ctx, root);
            setObjectState(ctx, root, StateObject.StateType.Ok);
David Sehnal's avatar
David Sehnal committed
            if (update.action === 'created') {
David Sehnal's avatar
David Sehnal committed
                ctx.stateCtx.events.object.created.next({ ref: root });
David Sehnal's avatar
David Sehnal committed
            } else if (update.action === 'updated') {
David Sehnal's avatar
David Sehnal committed
                ctx.stateCtx.events.object.updated.next({ ref: root });
David Sehnal's avatar
David Sehnal committed
            } else if (update.action === 'replaced') {
                ctx.stateCtx.events.object.replaced.next({ ref: root, old: update.old });
David Sehnal's avatar
David Sehnal committed
            }
        } catch (e) {
            doError(ctx, root, '' + e);
            return;
        }

        const children = ctx.tree.nodes.get(root)!.children.values();
        while (true) {
            const next = children.next();
            if (next.done) return;
David Sehnal's avatar
David Sehnal committed
            await updateSubtree(ctx, next.value);
David Sehnal's avatar
David Sehnal committed
    async function updateNode(ctx: UpdateContext, currentRef: Ref) {
David Sehnal's avatar
David Sehnal committed
        const { oldTree, tree, objects } = ctx;
        const transform = tree.getValue(currentRef)!;
        const parent = findParent(tree, objects, currentRef, transform.transformer.definition.from);
David Sehnal's avatar
David Sehnal committed
        // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined')
David Sehnal's avatar
David Sehnal committed
        if (!oldTree.nodes.has(currentRef) || !objects.has(currentRef)) {
David Sehnal's avatar
David Sehnal committed
            // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef));
David Sehnal's avatar
David Sehnal committed
            const obj = await createObject(ctx, transform.transformer, parent, transform.params);
            obj.ref = currentRef;
David Sehnal's avatar
David Sehnal committed
            objects.set(currentRef, {
                obj,
                state: StateObject.StateType.Ok,
                version: transform.version,
                props: { ...ctx.stateCtx.defaultObjectProps, ...transform.defaultProps }
            });
            return { action: 'created' };
David Sehnal's avatar
David Sehnal committed
            // console.log('updating...', transform.transformer.id);
David Sehnal's avatar
David Sehnal committed
            const current = objects.get(currentRef)!;
            const oldParams = oldTree.getValue(currentRef)!.params;
            switch (await updateObject(ctx, transform.transformer, parent, current.obj!, oldParams, transform.params)) {
                case Transformer.UpdateResult.Recreate: {
David Sehnal's avatar
David Sehnal committed
                    const obj = await createObject(ctx, transform.transformer, parent, transform.params);
                    obj.ref = currentRef;
David Sehnal's avatar
David Sehnal committed
                    objects.set(currentRef, {
                        obj,
                        state: StateObject.StateType.Ok,
                        version: transform.version,
                        props: { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps }
                    });
                    return { action: 'replaced', old: current.obj! };
David Sehnal's avatar
David Sehnal committed
                case Transformer.UpdateResult.Updated:
                    current.version = transform.version;
David Sehnal's avatar
David Sehnal committed
                    current.props = { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps };
                    return { action: 'updated' };
                default:
                    // TODO check if props need to be updated
                    return { action: 'none' };
David Sehnal's avatar
David Sehnal committed
    function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
        if (typeof (t as any).run === 'function') return (t as Task<T>).runInContext(ctx);
        return t as T;
    }

David Sehnal's avatar
David Sehnal committed
    function createObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, params: any) {
David Sehnal's avatar
David Sehnal committed
        return runTask(transformer.definition.apply({ a, params }, ctx.stateCtx.globalContext), ctx.taskCtx);
David Sehnal's avatar
David Sehnal committed
    async function updateObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
        if (!transformer.definition.update) {
            return Transformer.UpdateResult.Recreate;
David Sehnal's avatar
David Sehnal committed
        return runTask(transformer.definition.update({ a, oldParams, b, newParams }, ctx.stateCtx.globalContext), ctx.taskCtx);
David Sehnal's avatar
David Sehnal committed
    }
}