diff --git a/src/mol-plugin/ui/state-tree.tsx b/src/mol-plugin/ui/state-tree.tsx index 9a10114e2dfec52f6e281d878fff74061ec8fd6c..36fadc6ed4a474dd6d3d2faea408f1c9f98dbf36 100644 --- a/src/mol-plugin/ui/state-tree.tsx +++ b/src/mol-plugin/ui/state-tree.tsx @@ -13,7 +13,7 @@ import { PluginCommands } from 'mol-plugin/command'; export class StateTree extends React.Component<{ plugin: PluginContext, state: State }, { }> { componentDidMount() { // TODO: move to constructor? - this.props.state.events.updated.subscribe(() => this.forceUpdate()); + this.props.state.events.changed.subscribe(() => this.forceUpdate()); } render() { // const n = this.props.plugin.state.data.tree.nodes.get(this.props.plugin.state.data.tree.rootRef)!; diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 462b2d2e59005e5a6de4bb86c37f55d4f738abc1..c1ac80afa24ad3897136c0c6c1e2c6b3887bd802 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -29,6 +29,7 @@ class State { readonly events = { object: { cellState: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(), + cellCreated: this.ev<State.ObjectEvent>(), updated: this.ev<State.ObjectEvent & { obj?: StateObject }>(), replaced: this.ev<State.ObjectEvent & { oldObj?: StateObject, newObj?: StateObject }>(), @@ -36,7 +37,7 @@ class State { removed: this.ev<State.ObjectEvent & { obj?: StateObject }>() }, warn: this.ev<string>(), - updated: this.ev<void>() + changed: this.ev<void>() }; readonly behaviors = { @@ -84,7 +85,7 @@ class State { return StateSelection.select(selector(StateSelection.Generators), this) } - /** Is no ref is specified, apply to root */ + /** If no ref is specified, apply to root */ apply<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: Transform.Ref = Transform.RootRef): Task<void> { return Task.create('Apply Action', ctx => { const cell = this.cells.get(ref); @@ -96,9 +97,9 @@ class State { } update(tree: StateTree | StateTreeBuilder): Task<void> { - // TODO: support cell state const _tree = StateTreeBuilder.is(tree) ? tree.getTree() : tree; return Task.create('Update Tree', async taskCtx => { + let updated = false; try { const oldTree = this._tree; this._tree = _tree; @@ -109,12 +110,13 @@ class State { oldTree, tree: _tree, cells: this.cells as Map<Transform.Ref, StateObjectCell>, - transformCache: this.transformCache + transformCache: this.transformCache, + changed: false }; - // TODO: have "cancelled" error? Or would this be handled automatically? - await update(ctx); + // TODO: handle "cancelled" error? Or would this be handled automatically? + updated = await update(ctx); } finally { - this.events.updated.next(); + if (updated) this.events.changed.next(); } }); } @@ -164,11 +166,13 @@ interface UpdateContext { oldTree: StateTree, tree: StateTree, cells: Map<Transform.Ref, StateObjectCell>, - transformCache: Map<Ref, unknown> + transformCache: Map<Ref, unknown>, + changed: boolean } async function update(ctx: UpdateContext) { - const roots = findUpdateRoots(ctx.cells, ctx.tree); + // 1: find all nodes that will definitely be deleted. + // this is done in "post order", meaning that leaves will be deleted first. const deletes = findDeletes(ctx); for (const d of deletes) { const obj = ctx.cells.has(d) ? ctx.cells.get(d)!.obj : void 0; @@ -178,23 +182,33 @@ async function update(ctx: UpdateContext) { // TODO: handle current object change } + // 2: Find roots where transform version changed or where nodes will be added. + const roots = findUpdateRoots(ctx.cells, ctx.tree); + + // 3: Init empty cells where not present + // this is done in "pre order", meaning that "parents" will be created 1st. initCells(ctx, roots); + + // 4: Set status of cells that will be updated to 'pending'. initCellStatus(ctx, roots); + // 6: Sequentially update all the subtrees. for (const root of roots) { await updateSubtree(ctx, root); } + + return deletes.length > 0 || roots.length > 0 || ctx.changed; } function findUpdateRoots(cells: Map<Transform.Ref, StateObjectCell>, tree: StateTree) { const findState = { roots: [] as Ref[], cells }; - StateTree.doPreOrder(tree, tree.root, findState, _findUpdateRoots); + StateTree.doPreOrder(tree, tree.root, findState, findUpdateRootsVisitor); return findState.roots; } -function _findUpdateRoots(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) { +function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) { const cell = s.cells.get(n.ref); - if (!cell || cell.version !== n.version) { + if (!cell || cell.version !== n.version || cell.status === 'error') { s.roots.push(n.ref); return false; } @@ -219,19 +233,18 @@ function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Sta if (changed) ctx.parent.events.object.cellState.next({ state: ctx.parent, ref, cell }); } -function _initCellStatusVisitor(t: Transform, _: any, ctx: UpdateContext) { +function initCellStatusVisitor(t: Transform, _: any, ctx: UpdateContext) { ctx.cells.get(t.ref)!.transform = t; setCellStatus(ctx, t.ref, 'pending'); } -/** Return "resolve set" */ function initCellStatus(ctx: UpdateContext, roots: Ref[]) { for (const root of roots) { - StateTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, _initCellStatusVisitor); + StateTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, initCellStatusVisitor); } } -function _initCellsVisitor(transform: Transform, _: any, ctx: UpdateContext) { +function initCellsVisitor(transform: Transform, _: any, ctx: UpdateContext) { if (ctx.cells.has(transform.ref)) return; const obj: StateObjectCell = { @@ -242,38 +255,49 @@ function _initCellsVisitor(transform: Transform, _: any, ctx: UpdateContext) { errorText: void 0 }; ctx.cells.set(transform.ref, obj); - - // TODO: created event??? + ctx.parent.events.object.cellCreated.next({ state: ctx.parent, ref: transform.ref }); } function initCells(ctx: UpdateContext, roots: Ref[]) { for (const root of roots) { - StateTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, _initCellsVisitor); + StateTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, initCellsVisitor); } } -function doError(ctx: UpdateContext, ref: Ref, errorText: string) { - setCellStatus(ctx, ref, 'error', errorText); - const wrap = ctx.cells.get(ref)!; - if (wrap.obj) { - ctx.parent.events.object.removed.next({ state: ctx.parent, ref }); +/** Set status and error text of the cell. Remove all existing objects in the subtree. */ +function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) { + if (errorText) setCellStatus(ctx, ref, 'error', errorText); + + const cell = ctx.cells.get(ref)!; + if (cell.obj) { + const obj = cell.obj; + cell.obj = void 0; + ctx.parent.events.object.removed.next({ state: ctx.parent, ref, obj }); ctx.transformCache.delete(ref); - wrap.obj = void 0; } + // remove the objects in the child nodes if they exist 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.'); + doError(ctx, next.value, void 0); } } +type UpdateNodeResult = + | { action: 'created', obj: StateObject } + | { action: 'updated', obj: StateObject } + | { action: 'replaced', oldObj?: StateObject, newObj: StateObject } + | { action: 'none' } + async function updateSubtree(ctx: UpdateContext, root: Ref) { setCellStatus(ctx, root, 'processing'); try { const update = await updateNode(ctx, root); + if (update.action !== 'none') ctx.changed = true; + setCellStatus(ctx, root, 'ok'); if (update.action === 'created') { ctx.parent.events.object.created.next({ state: ctx.parent, ref: root, obj: update.obj! }); @@ -283,6 +307,7 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) { ctx.parent.events.object.replaced.next({ state: ctx.parent, ref: root, oldObj: update.oldObj, newObj: update.newObj }); } } catch (e) { + ctx.changed = true; doError(ctx, root, '' + e); return; } @@ -295,7 +320,7 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) { } } -async function updateNode(ctx: UpdateContext, currentRef: Ref) { +async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNodeResult> { const { oldTree, tree } = ctx; const transform = tree.nodes.get(currentRef); const parentCell = StateSelection.findAncestorOfType(tree, ctx.cells, currentRef, transform.transformer.definition.from); @@ -308,9 +333,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref) { const current = ctx.cells.get(currentRef)!; current.sourceRef = parentCell.transform.ref; - // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined') if (!oldTree.nodes.has(currentRef)) { - // 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; @@ -333,7 +356,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref) { } case Transformer.UpdateResult.Updated: current.version = transform.version; - return { action: 'updated', obj: current.obj }; + return { action: 'updated', obj: current.obj! }; default: return { action: 'none' }; } @@ -346,7 +369,7 @@ function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) { } function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) { - const cache = {}; + const cache = Object.create(null); ctx.transformCache.set(ref, cache); return runTask(transformer.definition.apply({ a, params, cache }, ctx.parent.globalContext), ctx.taskCtx); } @@ -357,7 +380,7 @@ async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transform } let cache = ctx.transformCache.get(ref); if (!cache) { - cache = {}; + cache = Object.create(null); ctx.transformCache.set(ref, cache); } return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.parent.globalContext), ctx.taskCtx);