Bladeren bron

use asset-manager for custom-properties

- obtain function needs to return a value and assets
- assets are stored per descriptor in model/structure
- assets shold be released via customProperties.dispose()
Alexander Rose 5 jaren geleden
bovenliggende
commit
bea4c64d85

+ 1 - 1
src/examples/basic-wrapper/coloring.ts

@@ -18,7 +18,7 @@ export const StripedResidues = CustomElementProperty.create<number>({
         for (let i = 0, _i = model.atomicHierarchy.atoms._rowCount; i < _i; i++) {
             map.set(i as ElementIndex, residueIndex[i] % 2);
         }
-        return map;
+        return { value: map };
     },
     coloring: {
         getColor(e) { return e === 0 ? Color(0xff0000) : Color(0x0000ff); },

+ 6 - 5
src/examples/proteopedia-wrapper/annotation.ts

@@ -9,6 +9,7 @@ import { CustomElementProperty } from '../../mol-model-props/common/custom-eleme
 import { Model, ElementIndex, ResidueIndex } from '../../mol-model/structure';
 import { Color } from '../../mol-util/color';
 import { CustomProperty } from '../../mol-model-props/common/custom-property';
+import { Asset } from '../../mol-util/assets';
 
 const EvolutionaryConservationPalette: Color[] = [
     [255, 255, 129], // insufficient
@@ -30,9 +31,9 @@ export const EvolutionaryConservation = CustomElementProperty.create<number>({
     type: 'static',
     async getData(model: Model, ctx: CustomProperty.Context) {
         const id = model.entryId.toLowerCase();
-        const url = `https://proteopedia.org/cgi-bin/cnsrf?${id}`;
-        const json = await ctx.fetch({ url, type: 'json' }).runInContext(ctx.runtime);
-        const annotations = (json && json.residueAnnotations) || [];
+        const url = Asset.getUrlAsset(ctx.assetManager, `https://proteopedia.org/cgi-bin/cnsrf?${id}`);
+        const json = await ctx.assetManager.resolve(url, 'json').runInContext(ctx.runtime);
+        const annotations = json.data?.residueAnnotations || [];
 
         const conservationMap = new Map<string, number>();
 
@@ -58,7 +59,7 @@ export const EvolutionaryConservation = CustomElementProperty.create<number>({
             }
         }
 
-        return map;
+        return { value: map, assets: [json] };
     },
     coloring: {
         getColor(e: number) {
@@ -68,7 +69,7 @@ export const EvolutionaryConservation = CustomElementProperty.create<number>({
         defaultColor: EvolutionaryConservationDefaultColor
     },
     getLabel(e) {
-        if (e === 10) return `Evolutionary Conservation: InsufficientData`;
+        if (e === 10) return `Evolutionary Conservation: Insufficient Data`;
         return e ? `Evolutionary Conservation: ${e}` : void 0;
     }
 });

+ 1 - 1
src/extensions/cellpack/model.ts

@@ -463,7 +463,7 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
 
         const structure = packing.obj?.data;
         if (structure) {
-            await CellPackInfoProvider.attach({ fetch: plugin.fetch, runtime }, structure, {
+            await CellPackInfoProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure, {
                 info: { packingsCount: packings.length, packingIndex: i }
             });
         }

+ 3 - 1
src/extensions/cellpack/property.ts

@@ -27,6 +27,8 @@ export const CellPackInfoProvider: CustomStructureProperty.Provider<typeof CellP
     getParams: (data: Structure) => CellPackInfoParams,
     isApplicable: (data: Structure) => true,
     obtain: async (ctx: CustomProperty.Context, data: Structure, props: CellPackInfoParams) => {
-        return { ...CellPackInfoParams.info.defaultValue, ...props.info };
+        return {
+            value: { ...CellPackInfoParams.info.defaultValue, ...props.info }
+        };
     }
 });

+ 9 - 7
src/extensions/pdbe/structure-quality-report/prop.ts

@@ -21,6 +21,7 @@ import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
 import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
 import { CustomProperty } from '../../../mol-model-props/common/custom-property';
 import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
+import { Asset } from '../../../mol-util/assets';
 
 export { StructureQualityReport };
 
@@ -67,12 +68,12 @@ namespace StructureQualityReport {
         return { info, data: issueMap };
     }
 
-    export async function fromServer(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): Promise<StructureQualityReport> {
-        const url = getEntryUrl(model.entryId, props.serverUrl);
-        const json = await ctx.fetch({ url, type: 'json' }).runInContext(ctx.runtime);
-        const data = json[model.entryId.toLowerCase()];
+    export async function fromServer(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): Promise<CustomProperty.Data<StructureQualityReport>> {
+        const url = Asset.getUrlAsset(ctx.assetManager, getEntryUrl(model.entryId, props.serverUrl));
+        const json = await ctx.assetManager.resolve(url, 'json').runInContext(ctx.runtime);
+        const data = json.data[model.entryId.toLowerCase()];
         if (!data) throw new Error('missing data');
-        return fromJson(model, data);
+        return { value: fromJson(model, data), assets: [json] };
     }
 
     export function fromCif(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): StructureQualityReport | undefined {
@@ -83,8 +84,9 @@ namespace StructureQualityReport {
         return { info, data: issueMap };
     }
 
-    export async function fromCifOrServer(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): Promise<StructureQualityReport> {
-        return fromCif(ctx, model, props) || fromServer(ctx, model, props);
+    export async function fromCifOrServer(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): Promise<CustomProperty.Data<StructureQualityReport>> {
+        const cif = fromCif(ctx, model, props);
+        return cif ? { value: cif } : fromServer(ctx, model, props);
     }
 
     const _emptyArray: string[] = [];

+ 4 - 4
src/extensions/rcsb/assembly-symmetry/behavior.ts

@@ -80,7 +80,7 @@ export const InitAssemblySymmetry3D = StateAction.build({
     isApplicable: (a) => AssemblySymmetry.isApplicable(a.data)
 })(({ a, ref, state }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => {
     try {
-        const propCtx = { runtime: ctx, fetch: plugin.fetch };
+        const propCtx = { runtime: ctx, assetManager: plugin.managers.asset };
         await AssemblySymmetryDataProvider.attach(propCtx, a.data);
         const assemblySymmetryData = AssemblySymmetryDataProvider.get(a.data).value;
         const symmetryIndex = assemblySymmetryData ? AssemblySymmetry.firstNonC1(assemblySymmetryData) : -1;
@@ -116,7 +116,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
     },
     apply({ a, params }, plugin: PluginContext) {
         return Task.create('Assembly Symmetry', async ctx => {
-            await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data);
+            await AssemblySymmetryProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
             const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value;
             if (!assemblySymmetry || assemblySymmetry.symbol === 'C1') {
                 return StateObject.Null;
@@ -129,7 +129,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
     },
     update({ a, b, newParams }, plugin: PluginContext) {
         return Task.create('Assembly Symmetry', async ctx => {
-            await AssemblySymmetryProvider.attach({ runtime: ctx, fetch: plugin.fetch }, a.data);
+            await AssemblySymmetryProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
             const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value;
             if (!assemblySymmetry || assemblySymmetry.symbol === 'C1') {
                 // this should NOT be StateTransformer.UpdateResult.Null
@@ -172,7 +172,7 @@ export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
 
         if (!AssemblySymmetryDataProvider.get(structure).value) {
             await plugin.runTask(Task.create('Assembly Symmetry', async runtime => {
-                const propCtx = { runtime, fetch: plugin.fetch };
+                const propCtx = { runtime, assetManager: plugin.managers.asset };
                 await AssemblySymmetryDataProvider.attach(propCtx, structure);
                 const assemblySymmetryData = AssemblySymmetryDataProvider.get(structure).value;
                 const symmetryIndex = assemblySymmetryData ? AssemblySymmetry.firstNonC1(assemblySymmetryData) : -1;

+ 10 - 8
src/extensions/rcsb/assembly-symmetry/prop.ts

@@ -57,21 +57,23 @@ export namespace AssemblySymmetry {
         );
     }
 
-    export async function fetch(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryDataProps): Promise<AssemblySymmetryDataValue> {
-        if (!isApplicable(structure)) return [];
+    export async function fetch(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryDataProps): Promise<CustomProperty.Data<AssemblySymmetryDataValue>> {
+        if (!isApplicable(structure)) return { value: [] };
 
-        const client = new GraphQLClient(props.serverUrl, ctx.fetch);
+        const client = new GraphQLClient(props.serverUrl, ctx.assetManager);
         const variables: AssemblySymmetryQueryVariables = {
             assembly_id: structure.units[0].conformation.operator.assembly?.id || 'deposited',
             entry_id: structure.units[0].model.entryId
         };
-        const result = await client.request<AssemblySymmetryQuery>(ctx.runtime, query, variables);
+        const result = await client.request(ctx.runtime, query, variables);
+        let value: AssemblySymmetryDataValue = [];
 
-        if (!result.assembly?.rcsb_struct_symmetry) {
+        if (!result.data.assembly?.rcsb_struct_symmetry) {
             console.error('expected `rcsb_struct_symmetry` field');
-            return [];
+        } else {
+            value = result.data.assembly.rcsb_struct_symmetry as AssemblySymmetryDataValue;
         }
-        return result.assembly.rcsb_struct_symmetry as AssemblySymmetryDataValue;
+        return { value, assets: [result] };
     }
 
     /** Returns the index of the first non C1 symmetry or -1 */
@@ -194,6 +196,6 @@ export const AssemblySymmetryProvider: CustomStructureProperty.Provider<Assembly
         const assemblySymmetryData = AssemblySymmetryDataProvider.get(data).value;
         const assemblySymmetry = assemblySymmetryData?.[p.symmetryIndex];
         if (!assemblySymmetry) new Error(`No assembly symmetry found for index ${p.symmetryIndex}`);
-        return assemblySymmetry;
+        return { value: assemblySymmetry };
     }
 });

+ 3 - 3
src/extensions/rcsb/validation-report/behavior.ts

@@ -314,7 +314,7 @@ export const ValidationReportGeometryQualityPreset = StructureRepresentationPres
         if (!structureCell || !model) return {};
 
         await plugin.runTask(Task.create('Validation Report', async runtime => {
-            await ValidationReportProvider.attach({ fetch: plugin.fetch, runtime }, model);
+            await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
         }));
 
         const colorTheme = GeometryQualityColorThemeProvider.name as any;
@@ -350,7 +350,7 @@ export const ValidationReportDensityFitPreset = StructureRepresentationPresetPro
         if (!structureCell || !model) return {};
 
         await plugin.runTask(Task.create('Validation Report', async runtime => {
-            await ValidationReportProvider.attach({ fetch: plugin.fetch, runtime }, model);
+            await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
         }));
 
         const colorTheme = DensityFitColorThemeProvider.name as any;
@@ -374,7 +374,7 @@ export const ValidationReportRandomCoilIndexPreset = StructureRepresentationPres
         if (!structureCell || !model) return {};
 
         await plugin.runTask(Task.create('Validation Report', async runtime => {
-            await ValidationReportProvider.attach({ fetch: plugin.fetch, runtime }, model);
+            await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
         }));
 
         const colorTheme = RandomCoilIndexColorThemeProvider.name as any;

+ 13 - 12
src/extensions/rcsb/validation-report/prop.ts

@@ -10,7 +10,6 @@ import { CustomProperty } from '../../../mol-model-props/common/custom-property'
 import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
 import { Model, ElementIndex, ResidueIndex } from '../../../mol-model/structure/model';
 import { IntAdjacencyGraph } from '../../../mol-math/graph';
-import { readFromFile } from '../../../mol-util/data-source';
 import { CustomStructureProperty } from '../../../mol-model-props/common/custom-structure-property';
 import { InterUnitGraph } from '../../../mol-math/graph/inter-unit-graph';
 import { UnitIndex } from '../../../mol-model/structure/structure/element/element';
@@ -22,6 +21,7 @@ import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
 import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
 import { CustomPropSymbol } from '../../../mol-script/language/symbol';
 import Type from '../../../mol-script/language/type';
+import { Asset } from '../../../mol-util/assets';
 
 export { ValidationReport };
 
@@ -104,20 +104,19 @@ namespace ValidationReport {
         return parseValidationReportXml(xml, model);
     }
 
-    export async function fetch(ctx: CustomProperty.Context, model: Model, props: ServerSourceProps): Promise<ValidationReport> {
-        const url = getEntryUrl(model.entryId, props.baseUrl);
-        const xml = await ctx.fetch({ url, type: 'xml' }).runInContext(ctx.runtime);
-        return fromXml(xml, model);
+    export async function fetch(ctx: CustomProperty.Context, model: Model, props: ServerSourceProps): Promise<CustomProperty.Data<ValidationReport>> {
+        const url = Asset.getUrlAsset(ctx.assetManager, getEntryUrl(model.entryId, props.baseUrl));
+        const xml = await ctx.assetManager.resolve(url, 'xml').runInContext(ctx.runtime);
+        return { value: fromXml(xml.data, model), assets: [xml] };
     }
 
-    export async function open(ctx: CustomProperty.Context, model: Model, props: FileSourceProps): Promise<ValidationReport> {
-        // TODO: this should use the asset manager and release the file "somehow"
-        if (!(props.input?.file instanceof File)) throw new Error('No file given');
-        const xml = await readFromFile(props.input.file, 'xml').runInContext(ctx.runtime);
-        return fromXml(xml, model);
+    export async function open(ctx: CustomProperty.Context, model: Model, props: FileSourceProps): Promise<CustomProperty.Data<ValidationReport>> {
+        if (props.input === null) throw new Error('No file given');
+        const xml = await ctx.assetManager.resolve(props.input, 'xml').runInContext(ctx.runtime);
+        return { value: fromXml(xml.data, model), assets: [xml] };
     }
 
-    export async function obtain(ctx: CustomProperty.Context, model: Model, props: ValidationReportProps): Promise<ValidationReport> {
+    export async function obtain(ctx: CustomProperty.Context, model: Model, props: ValidationReportProps): Promise<CustomProperty.Data<ValidationReport>> {
         switch(props.source.name) {
             case 'file': return open(ctx, model, props.source.params);
             case 'server': return fetch(ctx, model, props.source.params);
@@ -319,7 +318,9 @@ export const ClashesProvider: CustomStructureProperty.Provider<{}, Clashes> = Cu
     obtain: async (ctx: CustomProperty.Context, data: Structure) => {
         await ValidationReportProvider.attach(ctx, data.models[0]);
         const validationReport = ValidationReportProvider.get(data.models[0]).value!;
-        return createClashes(data, validationReport.clashes);
+        return {
+            value: createClashes(data, validationReport.clashes)
+        };
     }
 });
 

+ 2 - 1
src/mol-model-props/common/custom-element-property.ts

@@ -27,11 +27,12 @@ interface CustomElementProperty<T> {
 
 namespace CustomElementProperty {
     export type Value<T> = Map<ElementIndex, T>
+    export type Data<T> = CustomProperty.Data<Value<T>>
 
     export interface Builder<T> {
         label: string
         name: string
-        getData(model: Model, ctx?: CustomProperty.Context): Value<T> | Promise<Value<T>>
+        getData(model: Model, ctx?: CustomProperty.Context): Data<T> | Promise<Data<T>>
         coloring?: {
             getColor: (p: T) => Color
             defaultColor: Color

+ 5 - 2
src/mol-model-props/common/custom-model-property.ts

@@ -20,7 +20,7 @@ namespace CustomModelProperty {
         readonly defaultParams: Params
         readonly getParams: (data: Model) => Params
         readonly isApplicable: (data: Model) => boolean
-        readonly obtain: (ctx: CustomProperty.Context, data: Model, props: PD.Values<Params>) => Promise<Value>
+        readonly obtain: (ctx: CustomProperty.Context, data: Model, props: PD.Values<Params>) => Promise<CustomProperty.Data<Value>>
         readonly type: 'static' | 'dynamic'
     }
 
@@ -56,8 +56,9 @@ namespace CustomModelProperty {
                 const property = get(data);
                 const p = PD.merge(builder.defaultParams, property.props, props);
                 if (property.data.value && PD.areEqual(builder.defaultParams, property.props, p)) return;
-                const value = await builder.obtain(ctx, data, p);
+                const { value, assets } = await builder.obtain(ctx, data, p);
                 data.customProperties.add(builder.descriptor);
+                data.customProperties.assets(builder.descriptor, assets);
                 set(data, p, value);
             },
             ref: (data: Model, add: boolean) => data.customProperties.reference(builder.descriptor, add),
@@ -68,6 +69,8 @@ namespace CustomModelProperty {
                 if (!PD.areEqual(builder.defaultParams, property.props, p)) {
                     // this invalidates property.value
                     set(data, p, undefined);
+                    // dispose of assets
+                    data.customProperties.assets(builder.descriptor);
                 }
             },
             props: (data: Model) => get(data).props,

+ 4 - 3
src/mol-model-props/common/custom-property.ts

@@ -9,17 +9,18 @@ import { CustomPropertyDescriptor } from '../../mol-model/structure';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ValueBox } from '../../mol-util';
 import { OrderedMap } from 'immutable';
-
-type AjaxTask = import('../../mol-util/data-source').AjaxTask
+import { AssetManager, Asset } from '../../mol-util/assets';
 
 export { CustomProperty };
 
 namespace CustomProperty {
     export interface Context {
         runtime: RuntimeContext
-        fetch: AjaxTask
+        assetManager: AssetManager
     }
 
+    export type Data<V> = { value: V, assets?: Asset.Wrapper[] }
+
     export interface Container<P, V> {
         readonly props: P
         readonly data: ValueBox<V | undefined>

+ 5 - 2
src/mol-model-props/common/custom-structure-property.ts

@@ -20,7 +20,7 @@ namespace CustomStructureProperty {
         readonly defaultParams: Params
         readonly getParams: (data: Structure) => Params
         readonly isApplicable: (data: Structure) => boolean
-        readonly obtain: (ctx: CustomProperty.Context, data: Structure, props: PD.Values<Params>) => Promise<Value>
+        readonly obtain: (ctx: CustomProperty.Context, data: Structure, props: PD.Values<Params>) => Promise<CustomProperty.Data<Value>>
         readonly type: 'root' | 'local'
     }
 
@@ -58,8 +58,9 @@ namespace CustomStructureProperty {
                 const property = get(data);
                 const p = PD.merge(builder.defaultParams, rootProps, props);
                 if (property.data.value && PD.areEqual(builder.defaultParams, property.props, p)) return;
-                const value = await builder.obtain(ctx, data, p);
+                const { value, assets } = await builder.obtain(ctx, data, p);
                 data.customPropertyDescriptors.add(builder.descriptor);
+                data.customPropertyDescriptors.assets(builder.descriptor, assets);
                 set(data, p, value);
             },
             ref: (data: Structure, add: boolean) => data.customPropertyDescriptors.reference(builder.descriptor, add),
@@ -71,6 +72,8 @@ namespace CustomStructureProperty {
                 if (!PD.areEqual(builder.defaultParams, property.props, p)) {
                     // this invalidates property.value
                     set(data, p, value);
+                    // dispose of assets
+                    data.customPropertyDescriptors.assets(builder.descriptor);
                 }
             },
             props: (data: Structure) => get(data).props,

+ 1 - 1
src/mol-model-props/computed/accessible-surface-area.ts

@@ -54,6 +54,6 @@ export const AccessibleSurfaceAreaProvider: CustomStructureProperty.Provider<Acc
     isApplicable: (data: Structure) => true,
     obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<AccessibleSurfaceAreaProps>) => {
         const p = { ...PD.getDefaultValues(AccessibleSurfaceAreaParams), ...props };
-        return await AccessibleSurfaceArea.compute(data, p).runInContext(ctx.runtime);
+        return { value: await AccessibleSurfaceArea.compute(data, p).runInContext(ctx.runtime) };
     }
 });

+ 1 - 1
src/mol-model-props/computed/interactions.ts

@@ -30,6 +30,6 @@ export const InteractionsProvider: CustomStructureProperty.Provider<Interactions
     isApplicable: (data: Structure) => true,
     obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<InteractionsProps>) => {
         const p = { ...PD.getDefaultValues(InteractionsParams), ...props };
-        return await computeInteractions(ctx, data, p);
+        return { value: await computeInteractions(ctx, data, p) };
     }
 });

+ 2 - 2
src/mol-model-props/computed/secondary-structure.ts

@@ -57,8 +57,8 @@ export const SecondaryStructureProvider: CustomStructureProperty.Provider<Second
     obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<SecondaryStructureProps>) => {
         const p = { ...PD.getDefaultValues(SecondaryStructureParams), ...props };
         switch (p.type.name) {
-            case 'dssp': return await computeDssp(data, p.type.params);
-            case 'model': return await computeModel(data);
+            case 'dssp': return { value: await computeDssp(data, p.type.params) };
+            case 'model': return { value: await computeModel(data) };
         }
     }
 });

+ 1 - 1
src/mol-model-props/computed/valence-model.ts

@@ -30,6 +30,6 @@ export const ValenceModelProvider: CustomStructureProperty.Provider<ValenceModel
     isApplicable: (data: Structure) => true,
     obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<ValenceModelProps>) => {
         const p = { ...PD.getDefaultValues(ValenceModelParams), ...props };
-        return await calcValenceModel(ctx.runtime, data, p);
+        return { value: await calcValenceModel(ctx.runtime, data, p) };
     }
 });

+ 1 - 1
src/mol-model-props/integrative/cross-link-restraint/property.ts

@@ -29,7 +29,7 @@ export const CrossLinkRestraintProvider: CustomStructureProperty.Provider<{}, Cr
     getParams: (data: Structure) => ({}),
     isApplicable: (data: Structure) => data.models.some(m => !!ModelCrossLinkRestraint.Provider.get(m)),
     obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<{}>) => {
-        return extractCrossLinkRestraints(data);
+        return { value: extractCrossLinkRestraints(data) };
     }
 });
 

+ 22 - 6
src/mol-model/structure/common/custom-property.ts

@@ -1,13 +1,15 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { CifWriter } from '../../../mol-io/writer/cif';
 import { CifExportContext } from '../export/mmcif';
 import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
 import { UUID } from '../../../mol-util';
+import { Asset } from '../../../mol-util/assets';
 
 export { CustomPropertyDescriptor, CustomProperties };
 
@@ -42,6 +44,7 @@ class CustomProperties {
     private _list: CustomPropertyDescriptor[] = [];
     private _set = new Set<CustomPropertyDescriptor>();
     private _refs = new Map<CustomPropertyDescriptor, number>();
+    private _assets = new Map<CustomPropertyDescriptor, Asset.Wrapper[]>();
 
     get all(): ReadonlyArray<CustomPropertyDescriptor> {
         return this._list;
@@ -55,11 +58,7 @@ class CustomProperties {
     }
 
     reference(desc: CustomPropertyDescriptor<any>, add: boolean) {
-        let refs = this._refs.get(desc);
-        if (refs === void 0) {
-            refs = 0;
-            this._refs.set(desc, refs);
-        }
+        let refs = this._refs.get(desc) || 0;
         refs += add ? 1 : -1;
         this._refs.set(desc, Math.max(refs, 0));
     }
@@ -71,4 +70,21 @@ class CustomProperties {
     has(desc: CustomPropertyDescriptor<any>): boolean {
         return this._set.has(desc);
     }
+
+    /** Sets assets for a prop, disposes of existing assets for that prop */
+    assets(desc: CustomPropertyDescriptor<any>, assets?: Asset.Wrapper[]) {
+        const prevAssets = this._assets.get(desc);
+        if (prevAssets) {
+            for (const a of prevAssets) a.dispose();
+        }
+        if (assets) this._assets.set(desc, assets);
+        else this._assets.delete(desc);
+    }
+
+    /** Disposes of all assets of all props */
+    dispose() {
+        this._assets.forEach(assets => {
+            for (const a of assets) a.dispose();
+        });
+    }
 }

+ 1 - 1
src/mol-plugin-state/builder/structure.ts

@@ -173,7 +173,7 @@ export class StructureBuilder {
                 };
 
             if (selection.ensureCustomProperties) {
-                await selection.ensureCustomProperties({ fetch: this.plugin.fetch, runtime: taskCtx }, structureData);
+                await selection.ensureCustomProperties({ runtime: taskCtx, assetManager: this.plugin.managers.asset }, structureData);
             }
 
             return this.tryCreateComponent(structure, transformParams, key, tags);

+ 1 - 1
src/mol-plugin-state/helpers/structure-selection-query.ts

@@ -73,7 +73,7 @@ function StructureSelectionQuery(label: string, expression: Expression, props: S
             const current = plugin.managers.structure.selection.getStructure(structure);
             const currentSelection = current ? StructureSelection.Singletons(structure, current) : StructureSelection.Empty(structure);
             if (props.ensureCustomProperties) {
-                await props.ensureCustomProperties({ fetch: plugin.fetch, runtime }, structure);
+                await props.ensureCustomProperties({ runtime, assetManager: plugin.managers.asset }, structure);
             }
             if (!_query) _query = compile<StructureSelection>(expression);
             return _query(new QueryContext(structure, { currentSelection }));

+ 35 - 2
src/mol-plugin-state/transforms/model.ts

@@ -304,6 +304,9 @@ const ModelFromTrajectory = PluginStateTransform.BuiltIn({
         const label = `Model ${model.modelNum}`;
         const description = a.data.length === 1 ? undefined : `of ${a.data.length}`;
         return new SO.Molecule.Model(model, { label, description });
+    },
+    dispose({ b }) {
+        b?.data.customProperties.dispose();
     }
 });
 
@@ -320,6 +323,9 @@ const StructureFromTrajectory = PluginStateTransform.BuiltIn({
             const props = { label: 'Ensemble', description: Structure.elementDescription(s) };
             return new SO.Molecule.Structure(s, props);
         });
+    },
+    dispose({ b }) {
+        b?.data.customPropertyDescriptors.dispose();
     }
 });
 
@@ -343,6 +349,9 @@ const StructureFromModel = PluginStateTransform.BuiltIn({
         if (!b.data.models.includes(a.data)) return StateTransformer.UpdateResult.Recreate;
         if (!deepEqual(oldParams, newParams)) return StateTransformer.UpdateResult.Recreate;
         return StateTransformer.UpdateResult.Unchanged;
+    },
+    dispose({ b }) {
+        b?.data.customPropertyDescriptors.dispose();
     }
 });
 
@@ -439,6 +448,9 @@ const TransformStructureConformation = PluginStateTransform.BuiltIn({
 
         const s = Structure.transform(a.data, transform);
         return new SO.Molecule.Structure(s, { label: a.label, description: `${a.description} [Transformed]` });
+    },
+    dispose({ b }) {
+        b?.data.customPropertyDescriptors.dispose();
     }
     // interpolate(src, tar, t) {
     //     // TODO: optimize
@@ -489,6 +501,9 @@ const StructureSelectionFromExpression = PluginStateTransform.BuiltIn({
 
         StructureQueryHelper.updateStructureObject(b, selection, newParams.label);
         return StateTransformer.UpdateResult.Updated;
+    },
+    dispose({ b }) {
+        b?.data.customPropertyDescriptors.dispose();
     }
 });
 
@@ -650,6 +665,9 @@ const StructureSelectionFromScript = PluginStateTransform.BuiltIn({
         const selection = StructureQueryHelper.updateStructure(entry, a.data);
         StructureQueryHelper.updateStructureObject(b, selection, newParams.label);
         return StateTransformer.UpdateResult.Updated;
+    },
+    dispose({ b }) {
+        b?.data.customPropertyDescriptors.dispose();
     }
 });
 
@@ -698,6 +716,9 @@ const StructureSelectionFromBundle = PluginStateTransform.BuiltIn({
         b.description = Structure.elementDescription(s);
         b.data = s;
         return StateTransformer.UpdateResult.Updated;
+    },
+    dispose({ b }) {
+        b?.data.customPropertyDescriptors.dispose();
     }
 });
 
@@ -761,6 +782,9 @@ const StructureComplexElement = PluginStateTransform.BuiltIn({
 
         if (s.elementCount === 0) return StateObject.Null;
         return new SO.Molecule.Structure(s, { label, description: Structure.elementDescription(s) });
+    },
+    dispose({ b }) {
+        b?.data.customPropertyDescriptors.dispose();
     }
 });
 
@@ -777,6 +801,9 @@ const StructureComponent = PluginStateTransform.BuiltIn({
     },
     update: ({ a, b, oldParams, newParams, cache }) => {
         return updateStructureComponent(a.data, b, oldParams, newParams, cache as any);
+    },
+    dispose({ b }) {
+        b?.data.customPropertyDescriptors.dispose();
     }
 });
 
@@ -810,10 +837,13 @@ const CustomModelProperties = PluginStateTransform.BuiltIn({
             await attachModelProps(a.data, ctx, taskCtx, newParams);
             return StateTransformer.UpdateResult.Updated;
         });
+    },
+    dispose({ b }) {
+        b?.data.customProperties.dispose();
     }
 });
 async function attachModelProps(model: Model, ctx: PluginContext, taskCtx: RuntimeContext, params: ReturnType<CustomModelProperties['createDefaultParams']>) {
-    const propertyCtx = { runtime: taskCtx, fetch: ctx.fetch };
+    const propertyCtx = { runtime: taskCtx, assetManager: ctx.managers.asset };
     const { autoAttach, properties } = params;
     for (const name of Object.keys(properties)) {
         const property = ctx.customModelProperties.get(name);
@@ -860,10 +890,13 @@ const CustomStructureProperties = PluginStateTransform.BuiltIn({
             await attachStructureProps(a.data.root, ctx, taskCtx, newParams);
             return StateTransformer.UpdateResult.Updated;
         });
+    },
+    dispose({ b }) {
+        b?.data.customPropertyDescriptors.dispose();
     }
 });
 async function attachStructureProps(structure: Structure, ctx: PluginContext, taskCtx: RuntimeContext, params: ReturnType<CustomStructureProperties['createDefaultParams']>) {
-    const propertyCtx = { runtime: taskCtx, fetch: ctx.fetch };
+    const propertyCtx = { runtime: taskCtx, assetManager: ctx.managers.asset };
     const { autoAttach, properties } = params;
     for (const name of Object.keys(properties)) {
         const property = ctx.customStructureProperties.get(name);

+ 3 - 3
src/mol-plugin-state/transforms/representation.ts

@@ -120,7 +120,7 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
     },
     apply({ a, params, cache }, plugin: PluginContext) {
         return Task.create('Structure Representation', async ctx => {
-            const propertyCtx = { runtime: ctx, fetch: plugin.fetch };
+            const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset };
             const provider = plugin.representation.structure.registry.get(params.type.name);
             if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, a.data);
             const props = params.type.params || {};
@@ -149,7 +149,7 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
             Theme.releaseDependencies(plugin.representation.structure.themes, { structure: a.data }, oldParams);
 
             const provider = plugin.representation.structure.registry.get(newParams.type.name);
-            const propertyCtx = { runtime: ctx, fetch: plugin.fetch };
+            const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset };
             if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, a.data);
             const props = { ...b.data.repr.props, ...newParams.type.params };
             await Theme.ensureDependencies(propertyCtx, plugin.representation.structure.themes, { structure: a.data }, newParams);
@@ -524,7 +524,7 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
     },
     apply({ a, params }, plugin: PluginContext) {
         return Task.create('Volume Representation', async ctx => {
-            const propertyCtx = { runtime: ctx, fetch: plugin.fetch };
+            const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset };
             const provider = plugin.representation.volume.registry.get(params.type.name);
             if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, a.data);
             const props = params.type.params || {};

+ 15 - 25
src/mol-util/graphql-client.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  *
@@ -7,6 +7,7 @@
  */
 
 import { RuntimeContext } from '../mol-task';
+import { AssetManager, Asset } from './assets';
 
 type Variables = { [key: string]: any }
 
@@ -42,44 +43,33 @@ export class ClientError extends Error {
         this.request = request;
 
         // this is needed as Safari doesn't support .captureStackTrace
-        /* tslint:disable-next-line */
         if (typeof Error.captureStackTrace === 'function') {
             Error.captureStackTrace(this, ClientError);
         }
     }
 
     private static extractMessage (response: GraphQLResponse): string {
-        try {
-            return response.errors![0].message;
-        } catch (e) {
-            return `GraphQL Error (Code: ${response.status})`;
-        }
+        return response.errors ? response.errors[0].message : `GraphQL Error (Code: ${response.status})`;
     }
 }
 
 export class GraphQLClient {
-    constructor(private url: string, private fetch: import('../mol-util/data-source').AjaxTask) {
-        this.url = url;
-    }
-
-    async request<T extends any>(ctx: RuntimeContext, query: string, variables?: Variables): Promise<T> {
+    constructor(private url: string, private assetManager: AssetManager) { }
 
-        const body = JSON.stringify({
-            query,
-            variables: variables ? variables : undefined,
-        });
+    async request(ctx: RuntimeContext, query: string, variables?: Variables): Promise<Asset.Wrapper<'json'>> {
 
-        const resultStr = await this.fetch({ url: this.url, body }).runInContext(ctx);
-        const result = JSON.parse(resultStr);
+        const body = JSON.stringify({ query, variables }, null, 2);
+        const url = Asset.getUrlAsset(this.assetManager, this.url, body);
+        const result = await this.assetManager.resolve(url, 'json').runInContext(ctx);
 
-        if (!result.errors && result.data) {
-            return result.data;
+        if (!result.data.errors && result.data.data) {
+            return {
+                data: result.data.data,
+                dispose: result.dispose
+            };
         } else {
-            const errorResult = typeof result === 'string' ? { error: result } : result;
-            throw new ClientError(
-                { ...errorResult },
-                { query, variables },
-            );
+            const errorResult = typeof result.data === 'string' ? { error: result.data } : result.data;
+            throw new ClientError({ ...errorResult }, { query, variables });
         }
     }
 }

+ 2 - 2
src/tests/browser/render-structure.ts

@@ -25,7 +25,7 @@ import { InteractionsRepresentationProvider } from '../../mol-model-props/comput
 import { InteractionsProvider } from '../../mol-model-props/computed/interactions';
 import { SecondaryStructureProvider } from '../../mol-model-props/computed/secondary-structure';
 import { SyncRuntimeContext } from '../../mol-task/execution/synchronous';
-import { ajaxGet } from '../../mol-util/data-source';
+import { AssetManager } from '../../mol-util/assets';
 
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
@@ -117,7 +117,7 @@ function getGaussianSurfaceRepr() {
 }
 
 async function init() {
-    const ctx = { runtime: SyncRuntimeContext, fetch: ajaxGet };
+    const ctx = { runtime: SyncRuntimeContext, assetManager: new AssetManager() };
 
     const cif = await downloadFromPdb('3pqr');
     const models = await getModels(cif);