Skip to content
Snippets Groups Projects
state.ts 12.2 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>
 */

David Sehnal's avatar
David Sehnal committed
import { StateObject, StateObjectCell } from './object';
David Sehnal's avatar
David Sehnal committed
import { StateTree } from './tree';
David Sehnal's avatar
David Sehnal committed
import { Transform } from './transform';
import { Transformer } from './transformer';
David Sehnal's avatar
David Sehnal committed
import { UUID } from 'mol-util';
David Sehnal's avatar
David Sehnal committed
import { RuntimeContext, Task } from 'mol-task';
import { StateSelection } from './state/selection';
import { RxEventHelper } from 'mol-util/rx-event-helper';
David Sehnal's avatar
David Sehnal committed

export { State }
David Sehnal's avatar
David Sehnal committed

class State {
David Sehnal's avatar
David Sehnal committed
    private _tree: StateTree = StateTree.createEmpty();
David Sehnal's avatar
David Sehnal committed
    private _current: Transform.Ref = this._tree.root.ref;
David Sehnal's avatar
David Sehnal committed
    private transformCache = new Map<Transform.Ref, unknown>();

    private ev = RxEventHelper.create();

    readonly globalContext: unknown = void 0;
    readonly events = {
        object: {
David Sehnal's avatar
David Sehnal committed
            cellState: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
David Sehnal's avatar
David Sehnal committed
            updated: this.ev<State.ObjectEvent & { obj?: StateObject }>(),
            replaced: this.ev<State.ObjectEvent & { oldObj?: StateObject, newObj?: StateObject }>(),
            created: this.ev<State.ObjectEvent & { obj: StateObject }>(),
            removed: this.ev<State.ObjectEvent & { obj?: StateObject }>()
        },
        warn: this.ev<string>(),
        updated: this.ev<void>()
    };

    readonly behaviors = {
David Sehnal's avatar
David Sehnal committed
        currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: Transform.RootRef })
    get tree() { return this._tree; }
David Sehnal's avatar
David Sehnal committed
    get current() { return this._current; }
David Sehnal's avatar
David Sehnal committed

David Sehnal's avatar
David Sehnal committed
    readonly cells: State.Cells = new Map();
    getSnapshot(): State.Snapshot {
David Sehnal's avatar
David Sehnal committed
        return { tree: StateTree.toJSON(this._tree) };
    setSnapshot(snapshot: State.Snapshot) {
David Sehnal's avatar
David Sehnal committed
        const tree = StateTree.fromJSON(snapshot.tree);
David Sehnal's avatar
David Sehnal committed
        return this.update(tree);
David Sehnal's avatar
David Sehnal committed
    setCurrent(ref: Transform.Ref) {
        this._current = ref;
David Sehnal's avatar
David Sehnal committed
        this.behaviors.currentObject.next({ state: this, ref });
David Sehnal's avatar
David Sehnal committed
    }

David Sehnal's avatar
David Sehnal committed
    updateCellState(ref: Transform.Ref, state?: Partial<StateObjectCell.State>) {
        // TODO
    }

    dispose() {
        this.ev.dispose();
    /**
     * Select Cells by ref or a query generated on the fly.
     * @example state.select('test')
     * @example state.select(q => q.byRef('test').subtree())
     */
    select(selector: Transform.Ref | ((q: typeof StateSelection.Generators) => StateSelection.Selector)) {
        if (typeof selector === 'string') return StateSelection.select(selector, this);
        return StateSelection.select(selector(StateSelection.Generators), this)
David Sehnal's avatar
David Sehnal committed
    }

    query(q: StateSelection.Query) {
        return q(this);
David Sehnal's avatar
David Sehnal committed
    }

    update(tree: StateTree): Task<void> {
David Sehnal's avatar
David Sehnal committed
        // TODO: support cell state
David Sehnal's avatar
David Sehnal committed
        return Task.create('Update Tree', async taskCtx => {
            try {
                const oldTree = this._tree;
                this._tree = tree;

                const ctx: UpdateContext = {
David Sehnal's avatar
David Sehnal committed
                    taskCtx,
                    oldTree,
David Sehnal's avatar
David Sehnal committed
                    tree,
David Sehnal's avatar
David Sehnal committed
                    cells: this.cells as Map<Transform.Ref, StateObjectCell>,
David Sehnal's avatar
David Sehnal committed
                    transformCache: this.transformCache
                };
                // TODO: have "cancelled" error? Or would this be handled automatically?
                await update(ctx);
            } finally {
                this.events.updated.next();
David Sehnal's avatar
David Sehnal committed
            }
David Sehnal's avatar
David Sehnal committed
    constructor(rootObject: StateObject, params?: { globalContext?: unknown }) {
        const tree = this._tree;
David Sehnal's avatar
David Sehnal committed
        const root = tree.root;
David Sehnal's avatar
David Sehnal committed
        (this.cells as Map<Transform.Ref, StateObjectCell>).set(root.ref, {
            transform: root,
            sourceRef: void 0,
David Sehnal's avatar
David Sehnal committed
            obj: rootObject,
David Sehnal's avatar
David Sehnal committed
            status: 'ok',
            version: root.version
        this.globalContext = params && params.globalContext;
David Sehnal's avatar
David Sehnal committed
namespace State {
David Sehnal's avatar
David Sehnal committed
    export type Cells = ReadonlyMap<Transform.Ref, StateObjectCell>

    export interface ObjectEvent {
        state: State,
        ref: Ref
    }

    export interface Snapshot {
David Sehnal's avatar
David Sehnal committed
        readonly tree: StateTree.Serialized
David Sehnal's avatar
David Sehnal committed
    }
David Sehnal's avatar
David Sehnal committed

David Sehnal's avatar
David Sehnal committed
    export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) {
        return new State(rootObject, params);
    }
}

David Sehnal's avatar
David Sehnal committed
type Ref = Transform.Ref
David Sehnal's avatar
David Sehnal committed
interface UpdateContext {
David Sehnal's avatar
David Sehnal committed
    taskCtx: RuntimeContext,
    oldTree: StateTree,
    tree: StateTree,
David Sehnal's avatar
David Sehnal committed
    cells: Map<Transform.Ref, StateObjectCell>,
David Sehnal's avatar
David Sehnal committed
    transformCache: Map<Ref, unknown>
}
David Sehnal's avatar
David Sehnal committed
async function update(ctx: UpdateContext) {
    const roots = findUpdateRoots(ctx.cells, ctx.tree);
    const deletes = findDeletes(ctx);
    for (const d of deletes) {
        const obj = ctx.cells.has(d) ? ctx.cells.get(d)!.obj : void 0;
        ctx.cells.delete(d);
        ctx.transformCache.delete(d);
David Sehnal's avatar
David Sehnal committed
        ctx.parent.events.object.removed.next({ state: ctx.parent, ref: d, obj });
David Sehnal's avatar
David Sehnal committed
        // TODO: handle current object change
    }
David Sehnal's avatar
David Sehnal committed
    initCells(ctx, roots);
David Sehnal's avatar
David Sehnal committed
    initCellStatus(ctx, roots);
David Sehnal's avatar
David Sehnal committed

David Sehnal's avatar
David Sehnal committed
    for (const root of roots) {
        await updateSubtree(ctx, root);
David Sehnal's avatar
David Sehnal committed
    }
David Sehnal's avatar
David Sehnal committed
}
David Sehnal's avatar
David Sehnal committed

David Sehnal's avatar
David Sehnal committed
function findUpdateRoots(cells: Map<Transform.Ref, StateObjectCell>, tree: StateTree) {
David Sehnal's avatar
David Sehnal committed
    const findState = { roots: [] as Ref[], cells };
    StateTree.doPreOrder(tree, tree.root, findState, _findUpdateRoots);
    return findState.roots;
}

function _findUpdateRoots(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) {
    const cell = s.cells.get(n.ref);
    if (!cell || cell.version !== n.version) {
        s.roots.push(n.ref);
        return false;
David Sehnal's avatar
David Sehnal committed
    return true;
}
David Sehnal's avatar
David Sehnal committed
type FindDeletesCtx = { newTree: StateTree, cells: State.Cells, deletes: Ref[] }
function _visitCheckDelete(n: Transform, _: any, ctx: FindDeletesCtx) {
    if (!ctx.newTree.nodes.has(n.ref) && ctx.cells.has(n.ref)) ctx.deletes.push(n.ref);
}
function findDeletes(ctx: UpdateContext): Ref[] {
    const deleteCtx: FindDeletesCtx = { newTree: ctx.tree, cells: ctx.cells, deletes: [] };
    StateTree.doPostOrder(ctx.oldTree, ctx.oldTree.root, deleteCtx, _visitCheckDelete);
    return deleteCtx.deletes;
}
David Sehnal's avatar
David Sehnal committed
function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) {
    const cell = ctx.cells.get(ref)!;
    const changed = cell.status !== status;
    cell.status = status;
    cell.errorText = errorText;
    if (changed) ctx.parent.events.object.cellState.next({ state: ctx.parent, ref, cell });
David Sehnal's avatar
David Sehnal committed
}
David Sehnal's avatar
David Sehnal committed
function _initCellStatusVisitor(t: Transform, _: any, ctx: UpdateContext) {
    setCellStatus(ctx, t.ref, 'pending');
David Sehnal's avatar
David Sehnal committed
}
David Sehnal's avatar
David Sehnal committed

David Sehnal's avatar
David Sehnal committed
/** Return "resolve set" */
David Sehnal's avatar
David Sehnal committed
function initCellStatus(ctx: UpdateContext, roots: Ref[]) {
David Sehnal's avatar
David Sehnal committed
    for (const root of roots) {
David Sehnal's avatar
David Sehnal committed
        StateTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, _initCellStatusVisitor);
David Sehnal's avatar
David Sehnal committed
}
David Sehnal's avatar
David Sehnal committed
function _initCellsVisitor(transform: Transform, _: any, ctx: UpdateContext) {
    if (ctx.cells.has(transform.ref)) return;

    const obj: StateObjectCell = {
        transform,
        sourceRef: void 0,
David Sehnal's avatar
David Sehnal committed
        status: 'pending',
        version: UUID.create(),
        errorText: void 0
David Sehnal's avatar
David Sehnal committed
    };
    ctx.cells.set(transform.ref, obj);

    // TODO: created event???
}

function initCells(ctx: UpdateContext, roots: Ref[]) {
    for (const root of roots) {
        StateTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, _initCellsVisitor);
David Sehnal's avatar
David Sehnal committed
    }
David Sehnal's avatar
David Sehnal committed
}
David Sehnal's avatar
David Sehnal committed

David Sehnal's avatar
David Sehnal committed
function doError(ctx: UpdateContext, ref: Ref, errorText: string) {
David Sehnal's avatar
David Sehnal committed
    setCellStatus(ctx, ref, 'error', errorText);
David Sehnal's avatar
David Sehnal committed
    const wrap = ctx.cells.get(ref)!;
    if (wrap.obj) {
David Sehnal's avatar
David Sehnal committed
        ctx.parent.events.object.removed.next({ state: ctx.parent, ref });
David Sehnal's avatar
David Sehnal committed
        ctx.transformCache.delete(ref);
        wrap.obj = void 0;
    }
David Sehnal's avatar
David Sehnal committed

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

function findAncestor(tree: StateTree, cells: State.Cells, root: Ref, types: { type: StateObject.Type }[]): StateObjectCell | undefined {
David Sehnal's avatar
David Sehnal committed
    let current = tree.nodes.get(root)!;
    while (true) {
        current = tree.nodes.get(current.parent)!;
        const cell = cells.get(current.ref)!;
        if (!cell.obj) return void 0;
        for (const t of types) if (cell.obj.type === t.type) return cells.get(current.ref)!;
David Sehnal's avatar
David Sehnal committed
        if (current.ref === Transform.RootRef) {
David Sehnal's avatar
David Sehnal committed
}
David Sehnal's avatar
David Sehnal committed
async function updateSubtree(ctx: UpdateContext, root: Ref) {
David Sehnal's avatar
David Sehnal committed
    setCellStatus(ctx, root, 'processing');
David Sehnal's avatar
David Sehnal committed

    try {
        const update = await updateNode(ctx, root);
David Sehnal's avatar
David Sehnal committed
        setCellStatus(ctx, root, 'ok');
David Sehnal's avatar
David Sehnal committed
        if (update.action === 'created') {
David Sehnal's avatar
David Sehnal committed
            ctx.parent.events.object.created.next({ state: ctx.parent, ref: root, obj: update.obj! });
David Sehnal's avatar
David Sehnal committed
        } else if (update.action === 'updated') {
David Sehnal's avatar
David Sehnal committed
            ctx.parent.events.object.updated.next({ state: ctx.parent, ref: root, obj: update.obj });
David Sehnal's avatar
David Sehnal committed
        } else if (update.action === 'replaced') {
David Sehnal's avatar
David Sehnal committed
            ctx.parent.events.object.replaced.next({ state: ctx.parent, ref: root, oldObj: update.oldObj, newObj: update.newObj });
David Sehnal's avatar
David Sehnal committed
        }
David Sehnal's avatar
David Sehnal committed
    } catch (e) {
        doError(ctx, root, '' + e);
        return;
    }
David Sehnal's avatar
David Sehnal committed

David Sehnal's avatar
David Sehnal committed
    const children = ctx.tree.children.get(root).values();
    while (true) {
        const next = children.next();
        if (next.done) return;
        await updateSubtree(ctx, next.value);
David Sehnal's avatar
David Sehnal committed
}
David Sehnal's avatar
David Sehnal committed
async function updateNode(ctx: UpdateContext, currentRef: Ref) {
    const { oldTree, tree } = ctx;
David Sehnal's avatar
David Sehnal committed
    const transform = tree.nodes.get(currentRef);
    const parentCell = findAncestor(tree, ctx.cells, currentRef, transform.transformer.definition.from);

    if (!parentCell) {
        throw new Error(`No suitable parent found for '${currentRef}'`);
    }

    const parent = parentCell.obj!;
    const current = ctx.cells.get(currentRef)!;
    current.sourceRef = parentCell.transform.ref;

David Sehnal's avatar
David Sehnal committed
    // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined')
    if (!oldTree.nodes.has(currentRef)) {
David Sehnal's avatar
David Sehnal committed
        // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef));
        const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
        current.obj = obj;
        current.version = transform.version;
David Sehnal's avatar
David Sehnal committed

        return { action: 'created', obj };
    } else {
        const oldParams = oldTree.nodes.get(currentRef)!.params;

        const updateKind = current.status === 'ok' || current.transform.ref === Transform.RootRef
David Sehnal's avatar
David Sehnal committed
            ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params)
            : Transformer.UpdateResult.Recreate;

        switch (updateKind) {
            case Transformer.UpdateResult.Recreate: {
                const oldObj = current.obj;
                const newObj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
                current.obj = newObj;
                current.version = transform.version;
                return { action: 'replaced', oldObj, newObj: newObj };
David Sehnal's avatar
David Sehnal committed
            case Transformer.UpdateResult.Updated:
                current.version = transform.version;
                return { action: 'updated', obj: current.obj };
            default:
                return { action: 'none' };
David Sehnal's avatar
David Sehnal committed
}
David Sehnal's avatar
David Sehnal committed
function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
    if (typeof (t as any).runInContext === 'function') return (t as Task<T>).runInContext(ctx);
    return t as T;
}
David Sehnal's avatar
David Sehnal committed

David Sehnal's avatar
David Sehnal committed
function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) {
    const cache = {};
    ctx.transformCache.set(ref, cache);
    return runTask(transformer.definition.apply({ a, params, cache }, ctx.parent.globalContext), ctx.taskCtx);
David Sehnal's avatar
David Sehnal committed
}

async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
    if (!transformer.definition.update) {
        return Transformer.UpdateResult.Recreate;
    }
    let cache = ctx.transformCache.get(ref);
    if (!cache) {
        cache = {};
David Sehnal's avatar
David Sehnal committed
        ctx.transformCache.set(ref, cache);
    return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.parent.globalContext), ctx.taskCtx);
David Sehnal's avatar
David Sehnal committed
}