Bladeren bron

State now always queues update, plugin commands are always immedaite

David Sehnal 6 jaren geleden
bovenliggende
commit
ab8c6b5f3b

+ 2 - 2
src/apps/viewer/extensions/jolecule.ts

@@ -31,7 +31,7 @@ export const CreateJoleculeState = StateAction.build({
 
         data.sort((a, b) => a.order - b.order);
 
-        await PluginCommands.State.RemoveObject.dispatch(plugin, { state, ref }, true);
+        await PluginCommands.State.RemoveObject.dispatch(plugin, { state, ref });
         plugin.state.snapshots.clear();
 
         const template = createTemplate(plugin, state.tree, id);
@@ -40,7 +40,7 @@ export const CreateJoleculeState = StateAction.build({
             plugin.state.snapshots.add(s);
         }
 
-        PluginCommands.State.Snapshots.Apply.dispatch(plugin, { id: snapshots[0].snapshot.id }, true);
+        PluginCommands.State.Snapshots.Apply.dispatch(plugin, { id: snapshots[0].snapshot.id });
     } catch (e) {
         plugin.log.error(`Jolecule Failed: ${e}`);
     }

+ 1 - 1
src/mol-plugin/behavior/static/state.ts

@@ -76,7 +76,7 @@ export function RemoveObject(ctx: PluginContext) {
                 curr = parent;
             }
         } else {
-            remove(state, ref);
+            return remove(state, ref);
         }
     });
 }

+ 23 - 23
src/mol-plugin/command.ts

@@ -22,46 +22,46 @@ export const PluginCommands = {
 
         RemoveObject: PluginCommand<{ state: State, ref: StateTransform.Ref, removeParentGhosts?: boolean }>(),
 
-        ToggleExpanded: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }),
-        ToggleVisibility: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }),
-        Highlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }),
-        ClearHighlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }),
+        ToggleExpanded: PluginCommand<{ state: State, ref: StateTransform.Ref }>(),
+        ToggleVisibility: PluginCommand<{ state: State, ref: StateTransform.Ref }>(),
+        Highlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>(),
+        ClearHighlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>(),
 
         Snapshots: {
-            Add: PluginCommand<{ name?: string, description?: string, params?: PluginState.GetSnapshotParams }>({ isImmediate: true }),
-            Replace: PluginCommand<{ id: string, params?: PluginState.GetSnapshotParams }>({ isImmediate: true }),
-            Move: PluginCommand<{ id: string, dir: -1 | 1 }>({ isImmediate: true }),
-            Remove: PluginCommand<{ id: string }>({ isImmediate: true }),
-            Apply: PluginCommand<{ id: string }>({ isImmediate: true }),
-            Clear: PluginCommand<{}>({ isImmediate: true }),
+            Add: PluginCommand<{ name?: string, description?: string, params?: PluginState.GetSnapshotParams }>(),
+            Replace: PluginCommand<{ id: string, params?: PluginState.GetSnapshotParams }>(),
+            Move: PluginCommand<{ id: string, dir: -1 | 1 }>(),
+            Remove: PluginCommand<{ id: string }>(),
+            Apply: PluginCommand<{ id: string }>(),
+            Clear: PluginCommand<{}>(),
 
-            Upload: PluginCommand<{ name?: string, description?: string, serverUrl: string }>({ isImmediate: true }),
+            Upload: PluginCommand<{ name?: string, description?: string, serverUrl: string }>(),
             Fetch: PluginCommand<{ url: string }>(),
 
-            DownloadToFile: PluginCommand<{ name?: string }>({ isImmediate: true }),
-            OpenFile: PluginCommand<{ file: File }>({ isImmediate: true }),
+            DownloadToFile: PluginCommand<{ name?: string }>(),
+            OpenFile: PluginCommand<{ file: File }>(),
         }
     },
     Interactivity: {
         Structure: {
-            Highlight: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>({ isImmediate: true }),
-            Select: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>({ isImmediate: true })
+            Highlight: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>(),
+            Select: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>()
         }
     },
     Layout: {
-        Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>({ isImmediate: true })
+        Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>()
     },
     Camera: {
-        Reset: PluginCommand<{}>({ isImmediate: true }),
-        SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>({ isImmediate: true }),
+        Reset: PluginCommand<{}>(),
+        SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>(),
         Snapshots: {
-            Add: PluginCommand<{ name?: string, description?: string }>({ isImmediate: true }),
-            Remove: PluginCommand<{ id: string }>({ isImmediate: true }),
-            Apply: PluginCommand<{ id: string }>({ isImmediate: true }),
-            Clear: PluginCommand<{}>({ isImmediate: true }),
+            Add: PluginCommand<{ name?: string, description?: string }>(),
+            Remove: PluginCommand<{ id: string }>(),
+            Apply: PluginCommand<{ id: string }>(),
+            Clear: PluginCommand<{}>(),
         }
     },
     Canvas3D: {
-        SetSettings: PluginCommand<{ settings: Partial<Canvas3DProps> }>({ isImmediate: true })
+        SetSettings: PluginCommand<{ settings: Partial<Canvas3DProps> }>()
     }
 }

+ 11 - 52
src/mol-plugin/command/base.ts

@@ -5,33 +5,30 @@
  */
 
 import { PluginContext } from '../context';
-import { LinkedList } from 'mol-data/generic';
-import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { UUID } from 'mol-util';
 
 export { PluginCommand }
 
 interface PluginCommand<T = unknown> {
     readonly id: UUID,
-    dispatch(ctx: PluginContext, params: T, isChild?: boolean): Promise<void>,
-    subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription,
-    params: { isImmediate: boolean }
+    dispatch(ctx: PluginContext, params: T): Promise<void>,
+    subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription
 }
 
 /** namespace.id must a globally unique identifier */
-function PluginCommand<T>(params?: Partial<PluginCommand<T>['params']>): PluginCommand<T> {
-    return new Impl({ isImmediate: false, ...params });
+function PluginCommand<T>(): PluginCommand<T> {
+    return new Impl();
 }
 
 class Impl<T> implements PluginCommand<T> {
-    dispatch(ctx: PluginContext, params: T, isChild?: boolean): Promise<void> {
-        return ctx.commands.dispatch(this, params, isChild);
+    dispatch(ctx: PluginContext, params: T): Promise<void> {
+        return ctx.commands.dispatch(this, params);
     }
     subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription {
         return ctx.commands.subscribe(this, action);
     }
     id = UUID.create22();
-    constructor(public params: PluginCommand<T>['params']) {
+    constructor() {
     }
 }
 
@@ -43,23 +40,12 @@ namespace PluginCommand {
     }
 
     export type Action<T> = (params: T) => unknown | Promise<unknown>
-    type Instance = { cmd: PluginCommand<any>, params: any, isChild: boolean, resolve: () => void, reject: (e: any) => void }
+    type Instance = { cmd: PluginCommand<any>, params: any, resolve: () => void, reject: (e: any) => void }
 
     export class Manager {
         private subs = new Map<string, Action<any>[]>();
-        private queue = LinkedList<Instance>();
         private disposing = false;
 
-        private ev = RxEventHelper.create();
-
-        readonly behaviour = {
-            locked: this.ev.behavior<boolean>(false)
-        };
-
-        lock(locked: boolean = true) {
-            this.behaviour.locked.next(locked);
-        }
-
         subscribe<T>(cmd: PluginCommand<T>, action: Action<T>): Subscription {
             let actions = this.subs.get(cmd.id);
             if (!actions) {
@@ -84,7 +70,7 @@ namespace PluginCommand {
 
 
         /** Resolves after all actions have completed */
-        dispatch<T>(cmd: PluginCommand<T>, params: T, isChild = false) {
+        dispatch<T>(cmd: PluginCommand<T>, params: T) {
             return new Promise<void>((resolve, reject) => {
                 if (this.disposing) {
                     reject('disposed');
@@ -97,37 +83,22 @@ namespace PluginCommand {
                     return;
                 }
 
-                const instance: Instance = { cmd, params, resolve, reject, isChild };
-
-                if (cmd.params.isImmediate || isChild) {
-                    this.resolve(instance);
-                } else {
-                    this.queue.addLast(instance);
-                    this.next();
-                }
+                this.resolve({ cmd, params, resolve, reject });
             });
         }
 
         dispose() {
             this.subs.clear();
-            while (this.queue.count > 0) {
-                this.queue.removeFirst();
-            }
         }
 
         private async resolve(instance: Instance) {
             const actions = this.subs.get(instance.cmd.id);
             if (!actions) {
-                try {
-                    instance.resolve();
-                } finally {
-                    if (!instance.cmd.params.isImmediate && !this.disposing) this.next();
-                }
+                instance.resolve();
                 return;
             }
 
             try {
-                if (!instance.cmd.params.isImmediate && !instance.isChild) this.executing = true;
                 // TODO: should actions be called "asynchronously" ("setImmediate") instead?
                 for (const a of actions) {
                     await a(instance.params);
@@ -135,19 +106,7 @@ namespace PluginCommand {
                 instance.resolve();
             } catch (e) {
                 instance.reject(e);
-            } finally {
-                if (!instance.cmd.params.isImmediate && !instance.isChild) {
-                    this.executing = false;
-                    if (!this.disposing) this.next();
-                }
             }
         }
-
-        private executing = false;
-        private async next() {
-            if (this.queue.count === 0 || this.executing) return;
-            const instance = this.queue.removeFirst()!;
-            this.resolve(instance);
-        }
     }
 }

+ 1 - 3
src/mol-plugin/context.ts

@@ -76,9 +76,7 @@ export class PluginContext {
         },
         labels: {
             highlight: this.ev.behavior<{ entries: ReadonlyArray<LociLabelEntry> }>({ entries: [] })
-        },
-
-        command: this.commands.behaviour
+        }
     };
 
     readonly canvas3d: Canvas3D;

+ 4 - 10
src/mol-plugin/ui/viewport.tsx

@@ -12,7 +12,7 @@ import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
 import { Canvas3DParams } from 'mol-canvas3d/canvas3d';
 import { PluginLayoutStateParams } from 'mol-plugin/layout';
-import { ControlGroup } from './controls/common';
+import { ControlGroup, IconButton } from './controls/common';
 
 interface ViewportState {
     noWebGl: boolean
@@ -59,22 +59,16 @@ export class ViewportControls extends PluginUIComponent {
     }
 
     icon(name: string, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) {
-        return <button
-            className={`msp-btn msp-btn-link msp-btn-link-toggle-${isOn ? 'on' : 'off'}`}
-            onClick={onClick}
-            title={title}>
-                <span className={`msp-icon msp-icon-${name}`}/>
-            </button>
+        return <IconButton icon={name} toggleState={isOn} onClick={onClick} title={title} />;
     }
 
     render() {
-        // TODO: show some icons dimmed etc..
         return <div className={'msp-viewport-controls'}>
             <div className='msp-viewport-controls-buttons'>
                 {this.icon('reset-scene', this.resetCamera, 'Reset Camera')}<br/>
                 {this.icon('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.state.showControls)}<br/>
-                {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)}
-                {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}<br/>
+                {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)}<br />
+                {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}
             </div>
             {this.state.isSettingsExpanded &&
             <div className='msp-viewport-controls-scene-options'>

+ 37 - 17
src/mol-state/state.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
@@ -19,6 +19,7 @@ import { LogEntry } from 'mol-util/log-entry';
 import { now, formatTimespan } from 'mol-util/now';
 import { ParamDefinition } from 'mol-util/param-definition';
 import { StateTreeSpine } from './tree/spine';
+import { AsyncQueue } from 'mol-util/async-queue';
 
 export { State }
 
@@ -122,7 +123,7 @@ class State {
     }
 
     /**
-     * Reconcialites the existing state tree with the new version.
+     * Queues up a reconciliation of the existing state tree.
      *
      * If the tree is StateBuilder.To<T>, the corresponding StateObject is returned by the task.
      * @param tree Tree instance or a tree builder instance
@@ -131,27 +132,44 @@ class State {
     updateTree<T extends StateObject>(tree: StateBuilder.To<T>, options?: Partial<State.UpdateOptions>): Task<T>
     updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<void>
     updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<any> {
+        const params: UpdateParams = { tree, options };
         return Task.create('Update Tree', async taskCtx => {
-            this.events.isUpdating.next(true);
-            let updated = false;
-            const ctx = this.updateTreeAndCreateCtx(tree, taskCtx, options);
+            const ok = await this.updateQueue.enqueue(params);
+            if (!ok) return;
+
             try {
-                updated = await update(ctx);
-                if (StateBuilder.isTo(tree)) {
-                    const cell = this.select(tree.ref)[0];
-                    return cell && cell.obj;
-                }
+                const ret = await this._updateTree(taskCtx, params);
+                return ret;
             } finally {
-                this.spine.setSurrent();
+                this.updateQueue.handled(params);
+            }
+        }, () => {
+            this.updateQueue.remove(params);
+        });
+    }
 
-                if (updated) this.events.changed.next();
-                this.events.isUpdating.next(false);
+    private updateQueue = new AsyncQueue<UpdateParams>();
 
-                for (const ref of ctx.stateChanges) {
-                    this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) });
-                }
+    private async _updateTree(taskCtx: RuntimeContext, params: UpdateParams) {
+        this.events.isUpdating.next(true);
+        let updated = false;
+        const ctx = this.updateTreeAndCreateCtx(params.tree, taskCtx, params.options);
+        try {
+            updated = await update(ctx);
+            if (StateBuilder.isTo(params.tree)) {
+                const cell = this.select(params.tree.ref)[0];
+                return cell && cell.obj;
             }
-        });
+        } finally {
+            this.spine.setSurrent();
+
+            if (updated) this.events.changed.next();
+            this.events.isUpdating.next(false);
+
+            for (const ref of ctx.stateChanges) {
+                this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) });
+            }
+        }
     }
 
     private updateTreeAndCreateCtx(tree: StateTree | StateBuilder, taskCtx: RuntimeContext, options: Partial<State.UpdateOptions> | undefined) {
@@ -239,6 +257,8 @@ const StateUpdateDefaultOptions: State.UpdateOptions = {
 
 type Ref = StateTransform.Ref
 
+type UpdateParams = { tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions> }
+
 interface UpdateContext {
     parent: State,
     editInfo: StateBuilder.EditInfo | undefined

+ 19 - 1
src/mol-util/array.ts

@@ -56,4 +56,22 @@ export function arrayRms(array: NumberArray) {
 export function fillSerial<T extends NumberArray> (array: T, n?: number) {
     for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[ i ] = i
     return array
-}
+}
+
+export function arrayRemoveInPlace<T>(xs: T[], x: T) {
+    let i = 0, l = xs.length, found = false;
+    for (; i < l; i++) {
+        if (xs[i] === x) {
+            found = true;
+            break;
+        }
+    }
+    if (!found) return false;
+    i++;
+    for (; i < l; i++) {
+        xs[i] = xs[i - 1];
+    }
+    xs.pop();
+    return true;
+}
+(window as any).arrayRem = arrayRemoveInPlace

+ 42 - 0
src/mol-util/async-queue.ts

@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { arrayRemoveInPlace } from './array';
+import { Subject } from 'rxjs';
+
+export class AsyncQueue<T> {
+    private queue: T[] = [];
+    private signal = new Subject<{ v: T, removed: boolean }>();
+
+    enqueue(v: T) {
+        this.queue.push(v);
+        if (this.queue.length === 1) return true;
+        return this.waitFor(v);
+    }
+
+    handled(v: T) {
+        arrayRemoveInPlace(this.queue, v);
+        if (this.queue.length > 0) this.signal.next({ v: this.queue[0], removed: false });
+    }
+
+    remove(v: T) {
+        const rem = arrayRemoveInPlace(this.queue, v);
+        if (rem)
+        this.signal.next({ v, removed: true })
+        return rem;
+    }
+
+    private waitFor(t: T): Promise<boolean> {
+        return new Promise(res => {
+            const sub = this.signal.subscribe(({ v, removed }) => {
+                if (v === t) {
+                    sub.unsubscribe();
+                    res(removed);
+                }
+            });
+        })
+    }
+}