From d120039ef17a4647c5a304accd0d99fa6985b403 Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Tue, 6 Nov 2018 13:22:14 +0100
Subject: [PATCH] wip state & plugin

---
 src/mol-plugin/behaviour.ts                | 21 +++++++++++++
 src/mol-plugin/command.ts                  |  2 ++
 src/mol-plugin/context.ts                  |  9 ++++++
 src/mol-plugin/state/transforms/data.ts    | 25 ++++++++++++----
 src/mol-plugin/state/transforms/model.ts   | 20 ++++++++++---
 src/mol-plugin/state/transforms/visuals.ts |  5 ++--
 src/mol-state/object.ts                    |  3 +-
 src/mol-state/state.ts                     | 33 +++++++++++++-------
 src/mol-state/transformer.ts               | 35 ++++++++++++----------
 9 files changed, 114 insertions(+), 39 deletions(-)
 create mode 100644 src/mol-plugin/command.ts

diff --git a/src/mol-plugin/behaviour.ts b/src/mol-plugin/behaviour.ts
index e69de29bb..e8025a76a 100644
--- a/src/mol-plugin/behaviour.ts
+++ b/src/mol-plugin/behaviour.ts
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+export { PluginBehaviour }
+
+interface PluginBehaviour<P> {
+    register(): void,
+    unregister(): void,
+
+    /** Update params in place. Optionally return a promise if it depends on an async action. */
+    update(params: P): void | Promise<void>
+}
+
+namespace PluginBehaviour {
+    export interface Ctor<P> {
+        create(params: P): PluginBehaviour<P>
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts
new file mode 100644
index 000000000..452c8ac07
--- /dev/null
+++ b/src/mol-plugin/command.ts
@@ -0,0 +1,2 @@
+// TODO: command interface and queue.
+// How to handle command resolving? Track how many subscriptions a command has?
\ No newline at end of file
diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts
index 4b2fc4a1c..c5e7d6846 100644
--- a/src/mol-plugin/context.ts
+++ b/src/mol-plugin/context.ts
@@ -35,6 +35,15 @@ export class PluginContext {
         }
     }
 
+    /**
+     * This should be used in all transform related request so that it could be "spoofed" to allow
+     * "static" access to resources.
+     */
+    async fetch(url: string, type: 'string' | 'binary' = 'string'): Promise<string | Uint8Array> {
+        const req = await fetch(url);
+        return type === 'string' ? await req.text() : new Uint8Array(await req.arrayBuffer());
+    }
+
     dispose() {
         if (this.disposed) return;
         this.canvas3d.dispose();
diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts
index e405712b1..6ac2fa935 100644
--- a/src/mol-plugin/state/transforms/data.ts
+++ b/src/mol-plugin/state/transforms/data.ts
@@ -8,24 +8,37 @@ import { PluginStateTransform } from '../base';
 import { PluginStateObjects as SO } from '../objects';
 import { Task } from 'mol-task';
 import CIF from 'mol-io/reader/cif'
+import { PluginContext } from 'mol-plugin/context';
 
-export const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, { url: string, isBinary?: boolean, label?: string }>({
+export { Download }
+namespace Download { export interface Params { url: string, isBinary?: boolean, label?: string } }
+const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, Download.Params>({
     name: 'download',
+    display: {
+        name: 'Download',
+        description: 'Download string or binary data from the specified URL'
+    },
     from: [SO.Root],
     to: [SO.Data.String, SO.Data.Binary],
-    apply({ params: p }) {
+    apply({ params: p }, globalCtx: PluginContext) {
         return Task.create('Download', async ctx => {
             // TODO: track progress
-            const req = await fetch(p.url);
+            const data = await globalCtx.fetch(p.url, p.isBinary ? 'binary' : 'string');
             return p.isBinary
-                ? new SO.Data.Binary({ label: p.label ? p.label : p.url }, new Uint8Array(await req.arrayBuffer()))
-                : new SO.Data.String({ label: p.label ? p.label : p.url }, await req.text());
+                ? new SO.Data.Binary({ label: p.label ? p.label : p.url }, data as Uint8Array)
+                : new SO.Data.String({ label: p.label ? p.label : p.url }, data as string);
         });
     }
 });
 
-export const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO.Data.Cif, { }>({
+export { ParseCif }
+namespace ParseCif { export interface Params { } }
+const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO.Data.Cif, ParseCif.Params>({
     name: 'parse-cif',
+    display: {
+        name: 'Parse CIF',
+        description: 'Parse CIF from String or Binary data'
+    },
     from: [SO.Data.String, SO.Data.Binary],
     to: [SO.Data.Cif],
     apply({ a }) {
diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts
index 419894e9d..e75e0455b 100644
--- a/src/mol-plugin/state/transforms/model.ts
+++ b/src/mol-plugin/state/transforms/model.ts
@@ -9,11 +9,17 @@ import { PluginStateObjects as SO } from '../objects';
 import { Task } from 'mol-task';
 import { Model, Format, Structure } from 'mol-model/structure';
 
-export const CreateModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Models, { blockHeader?: string }>({
+export { CreateModelsFromMmCif }
+namespace CreateModelsFromMmCif { export interface Params { blockHeader?: string } }
+const CreateModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Models, CreateModelsFromMmCif.Params>({
     name: 'create-models-from-mmcif',
+    display: {
+        name: 'Models from mmCIF',
+        description: 'Identify and create all separate models in the specified CIF data block'
+    },
     from: [SO.Data.Cif],
     to: [SO.Models],
-    defaultParams: a => ({ blockHeader: a.data.blocks[0].header }),
+    params: { default: a => ({ blockHeader: a.data.blocks[0].header }) },
     apply({ a, params }) {
         return Task.create('Parse mmCIF', async ctx => {
             const header = params.blockHeader || a.data.blocks[0].header;
@@ -27,11 +33,17 @@ export const CreateModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO
     }
 });
 
-export const CreateStructureFromModel = PluginStateTransform.Create<SO.Models, SO.Structure, { modelIndex: number }>({
+export { CreateStructureFromModel }
+namespace CreateStructureFromModel { export interface Params { modelIndex: number } }
+const CreateStructureFromModel = PluginStateTransform.Create<SO.Models, SO.Structure, CreateStructureFromModel.Params>({
     name: 'structure-from-model',
+    display: {
+        name: 'Structure from Model',
+        description: 'Create a molecular structure from the specified model.'
+    },
     from: [SO.Models],
     to: [SO.Structure],
-    defaultParams: () => ({ modelIndex: 0 }),
+    params: { default: () => ({ modelIndex: 0 }) },
     apply({ a, params }) {
         if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
         // TODO: make Structure.ofModel async?
diff --git a/src/mol-plugin/state/transforms/visuals.ts b/src/mol-plugin/state/transforms/visuals.ts
index f03e8bdc2..80e65bd79 100644
--- a/src/mol-plugin/state/transforms/visuals.ts
+++ b/src/mol-plugin/state/transforms/visuals.ts
@@ -10,11 +10,12 @@ import { PluginStateTransform } from '../base';
 import { PluginStateObjects as SO } from '../objects';
 import { CartoonRepresentation, DefaultCartoonProps } from 'mol-repr/structure/representation/cartoon';
 
-export const CreateStructureRepresentation = PluginStateTransform.Create<SO.Structure, SO.StructureRepresentation3D, { }>({
+export { CreateStructureRepresentation }
+namespace CreateStructureRepresentation { export interface Params { } }
+const CreateStructureRepresentation = PluginStateTransform.Create<SO.Structure, SO.StructureRepresentation3D, CreateStructureRepresentation.Params>({
     name: 'create-structure-representation',
     from: [SO.Structure],
     to: [SO.StructureRepresentation3D],
-    defaultParams: () => ({ modelIndex: 0 }),
     apply({ a, params }) {
         return Task.create('Structure Representation', async ctx => {
             const repr = CartoonRepresentation();
diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts
index aed685c1a..dddfcb68b 100644
--- a/src/mol-state/object.ts
+++ b/src/mol-state/object.ts
@@ -9,7 +9,7 @@ import { Transform } from './transform';
 import { UUID } from 'mol-util';
 
 /** A mutable state object */
-export interface StateObject<P = unknown, D = unknown> {
+export interface StateObject<P = any, D = any> {
     readonly id: UUID,
     readonly type: StateObject.Type,
     readonly props: P,
@@ -45,7 +45,6 @@ export namespace StateObject {
             static is(obj?: StateObject): obj is StateObject<Props, Data> { return !!obj && dataType === obj.type; }
             id = UUID.create();
             type = dataType;
-            ref = 'not set' as Transform.Ref;
             constructor(public props: Props, public data: Data) { }
         }
     }
diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts
index d59f98ce0..0a4932e30 100644
--- a/src/mol-state/state.ts
+++ b/src/mol-state/state.ts
@@ -17,6 +17,8 @@ export { State }
 
 class State {
     private _tree: StateTree = StateTree.create();
+    private transformCache = new Map<Transform.Ref, unknown>();
+
     get tree() { return this._tree; }
 
     readonly objects: State.Objects = new Map();
@@ -44,7 +46,8 @@ class State {
                 taskCtx,
                 oldTree,
                 tree: tree,
-                objects: this.objects
+                objects: this.objects,
+                transformCache: this.transformCache
             };
             // TODO: have "cancelled" error? Or would this be handled automatically?
             return update(ctx);
@@ -71,7 +74,7 @@ class State {
     }
 }
 
-namespace State {    
+namespace State {
     export type Objects = Map<Transform.Ref, StateObject.Node>
 
     export interface Snapshot {
@@ -91,7 +94,8 @@ namespace State {
         taskCtx: RuntimeContext,
         oldTree: StateTree,
         tree: StateTree,
-        objects: State.Objects
+        objects: State.Objects,
+        transformCache: Map<Ref, unknown>
     }
 
     async function update(ctx: UpdateContext) {
@@ -99,6 +103,7 @@ namespace State {
         const deletes = findDeletes(ctx);
         for (const d of deletes) {
             ctx.objects.delete(d);
+            ctx.transformCache.delete(d);
             ctx.stateCtx.events.object.removed.next({ ref: d });
         }
 
@@ -174,6 +179,7 @@ namespace State {
         const wrap = ctx.objects.get(ref)!;
         if (wrap.obj) {
             ctx.stateCtx.events.object.removed.next({ ref });
+            ctx.transformCache.delete(ref);
             wrap.obj = void 0;
         }
 
@@ -230,7 +236,7 @@ namespace State {
         // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined')
         if (!oldTree.nodes.has(currentRef) || !objects.has(currentRef)) {
             // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef));
-            const obj = await createObject(ctx, transform.transformer, parent, transform.params);
+            const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
             objects.set(currentRef, {
                 ref: currentRef,
                 obj,
@@ -243,9 +249,9 @@ namespace State {
             // console.log('updating...', transform.transformer.id);
             const current = objects.get(currentRef)!;
             const oldParams = oldTree.getValue(currentRef)!.params;
-            switch (await updateObject(ctx, transform.transformer, parent, current.obj!, oldParams, transform.params)) {
+            switch (await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params)) {
                 case Transformer.UpdateResult.Recreate: {
-                    const obj = await createObject(ctx, transform.transformer, parent, transform.params);
+                    const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
                     objects.set(currentRef, {
                         ref: currentRef,
                         obj,
@@ -271,13 +277,20 @@ namespace State {
         return t as T;
     }
 
-    function createObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, params: any) {
-        return runTask(transformer.definition.apply({ a, params }, ctx.stateCtx.globalContext), ctx.taskCtx);
+    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.stateCtx.globalContext), ctx.taskCtx);
     }
 
-    async function updateObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
+    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;
         }
-        return runTask(transformer.definition.update({ a, oldParams, b, newParams }, ctx.stateCtx.globalContext), ctx.taskCtx);
+        let cache = ctx.transformCache.get(ref);
+        if (!cache) {
+            cache = { };
+            ctx.transformCache.set(ref, cache);
+        }
+        return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.stateCtx.globalContext), ctx.taskCtx);
     }
\ No newline at end of file
diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts
index 972d21833..6b9e0c7ab 100644
--- a/src/mol-state/transformer.ts
+++ b/src/mol-state/transformer.ts
@@ -7,6 +7,7 @@
 import { Task } from 'mol-task';
 import { StateObject } from './object';
 import { Transform } from './transform';
+import { Param } from 'mol-util/parameter';
 
 export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
     apply(params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>,
@@ -19,18 +20,22 @@ 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 To<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? B : unknown;
-    export type ControlsFor<Props> = { [P in keyof Props]?: any }
+    export type ControlsFor<A extends StateObject, Props> = { [P in keyof Props]?: ((a: A, globalCtx: unknown) => Param) }
 
     export interface ApplyParams<A extends StateObject = StateObject, P = unknown> {
         a: A,
-        params: P
+        params: P,
+        /** A cache object that is purged each time the corresponding StateObject is removed or recreated. */
+        cache: unknown
     }
 
     export interface UpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
         a: A,
         b: B,
         oldParams: P,
-        newParams: P
+        newParams: P,
+        /** A cache object that is purged each time the corresponding StateObject is removed or recreated. */
+        cache: unknown
     }
 
     export enum UpdateResult { Unchanged, Updated, Recreate }
@@ -39,6 +44,7 @@ export namespace Transformer {
         readonly name: string,
         readonly from: { type: StateObject.Type }[],
         readonly to: { type: StateObject.Type }[],
+        readonly display?: { readonly name: string, readonly description?: string },
 
         /**
          * Apply the actual transformation. It must be pure (i.e. with no side effects).
@@ -53,17 +59,16 @@ export namespace Transformer {
          */
         update?(params: UpdateParams<A, B, P>, globalCtx: unknown): Task<UpdateResult> | UpdateResult,
 
-        /** Check the parameters and return a list of errors if the are not valid. */
-        defaultParams?(a: A, globalCtx: unknown): P,
-
-        /** Specify default control descriptors for the parameters */
-        defaultControls?(a: A, globalCtx: unknown): Transformer.ControlsFor<P>,
-
-        /** Check the parameters and return a list of errors if the are not valid. */
-        validateParams?(a: A, params: P, globalCtx: unknown): string[] | undefined,
-
-        /** Optional custom parameter equality. Use deep structural equal by default. */
-        areParamsEqual?(oldParams: P, newParams: P): boolean,
+        params?: {
+            /** Check the parameters and return a list of errors if the are not valid. */
+            default?(a: A, globalCtx: unknown): P,
+            /** Specify default control descriptors for the parameters */
+            controls?(a: A, globalCtx: unknown): ControlsFor<A, P>,
+            /** Check the parameters and return a list of errors if the are not valid. */
+            validate?(a: A, params: P, globalCtx: unknown): string[] | undefined,
+            /** Optional custom parameter equality. Use deep structural equal by default. */
+            areEqual?(oldParams: P, newParams: P): boolean
+        }
 
         /** Test if the transform can be applied to a given node */
         isApplicable?(a: A, globalCtx: unknown): boolean,
@@ -75,7 +80,7 @@ export namespace Transformer {
         customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P }
     }
 
-    const registry = new Map<Id, Transformer>();
+    const registry = new Map<Id, Transformer<any, any>>();
 
     export function get(id: string): Transformer {
         const t = registry.get(id as Id);
-- 
GitLab