Procházet zdrojové kódy

State transactions, better error message for failed downloads

David Sehnal před 5 roky
rodič
revize
9e5cc184ed

+ 16 - 14
src/mol-plugin-state/actions/structure.ts

@@ -234,25 +234,27 @@ const DownloadStructure = StateAction.build({
 
     const createRepr = !params.source.params.structure.noRepresentation;
 
-    if (downloadParams.length > 0 && asTrajectory) {
-        const traj = await createSingleTrajectoryModel(plugin, state, downloadParams);
-        const struct = createStructure(state.build().to(traj), supportProps, src.params.structure.type);
-        await state.updateTree(struct, { revertIfAborted: true }).runInContext(ctx);
-        if (createRepr) {
-            await plugin.structureRepresentation.manager.apply(struct.ref, plugin.structureRepresentation.manager.defaultProvider);
-        }
-    } else {
-        for (const download of downloadParams) {
-            const data = await plugin.builders.data.download(download, { state: { isGhost: true } });
-            const traj = createModelTree(state.build().to(data), format);
-
-            const struct = createStructure(traj, supportProps, src.params.structure.type);
+    state.transaction(async () => {
+        if (downloadParams.length > 0 && asTrajectory) {
+            const traj = await createSingleTrajectoryModel(plugin, state, downloadParams);
+            const struct = createStructure(state.build().to(traj), supportProps, src.params.structure.type);
             await state.updateTree(struct, { revertIfAborted: true }).runInContext(ctx);
             if (createRepr) {
                 await plugin.structureRepresentation.manager.apply(struct.ref, plugin.structureRepresentation.manager.defaultProvider);
             }
+        } else {
+            for (const download of downloadParams) {
+                const data = await plugin.builders.data.download(download, { state: { isGhost: true } });
+                const traj = createModelTree(state.build().to(data), format);
+
+                const struct = createStructure(traj, supportProps, src.params.structure.type);
+                await state.updateTree(struct, { revertIfAborted: true }).runInContext(ctx);
+                if (createRepr) {
+                    await plugin.structureRepresentation.manager.apply(struct.ref, plugin.structureRepresentation.manager.defaultProvider);
+                }
+            }
         }
-    }
+    }).runInContext(ctx);
 }));
 
 function getDownloadParams(src: string, url: (id: string) => string, label: (id: string) => string, isBinary: boolean): StateTransformer.Params<Download>[] {

+ 4 - 4
src/mol-plugin-state/builder/data.ts

@@ -16,26 +16,26 @@ export class DataBuilder {
 
     async rawData(params: StateTransformer.Params<RawData>, options?: Partial<StateTransform.Options>) {
         const data = this.dataState.build().toRoot().apply(RawData, params, options);
-        await this.plugin.runTask(this.dataState.updateTree(data));
+        await this.plugin.runTask(this.dataState.updateTree(data, { revertOnError: true }));
         return data.selector;
     }
 
     async download(params: StateTransformer.Params<Download>, options?: Partial<StateTransform.Options>) {
         const data = this.dataState.build().toRoot().apply(Download, params, options);
-        await this.plugin.runTask(this.dataState.updateTree(data));
+        await this.plugin.runTask(this.dataState.updateTree(data, { revertOnError: true }));
         return data.selector;
     }
 
     async downloadBlob(params: StateTransformer.Params<DownloadBlob>, options?: Partial<StateTransform.Options>) {        
         const data = this.dataState.build().toRoot().apply(DownloadBlob, params, options);
-        await this.plugin.runTask(this.dataState.updateTree(data));
+        await this.plugin.runTask(this.dataState.updateTree(data, { revertOnError: true }));
         return data.selector;
     }
 
     async readFile(params: StateTransformer.Params<ReadFile>, options?: Partial<StateTransform.Options>) {
         const data = this.dataState.build().toRoot().apply(ReadFile, params, options);
         const fileInfo = getFileInfo(params.file);
-        await this.plugin.runTask(this.dataState.updateTree(data));
+        await this.plugin.runTask(this.dataState.updateTree(data, { revertOnError: true }));
         return { data: data.selector, fileInfo };
     }
 

+ 29 - 6
src/mol-state/state.ts

@@ -126,6 +126,26 @@ class State {
         });
     }
 
+    /** Apply series of updates to the state. If any of them fail, revert to the original state. */
+    transaction(edits: () => Promise<void> | void) {
+        return Task.create('State Transaction', async ctx => {
+            const snapshot = this._tree.asImmutable();
+            let restored = false;
+            try {
+                await edits();
+
+                let hasError = false;
+                this.cells.forEach(c => hasError = hasError || c.state === 'error');
+                if (hasError) {
+                    restored = true;
+                    this.updateTree(snapshot).runInContext(ctx);
+                }
+            } catch (e) {
+                if (!restored) this.updateTree(snapshot).runInContext(ctx);
+            }
+        });
+    }
+
     /**
      * Queues up a reconciliation of the existing state tree.
      *
@@ -142,8 +162,8 @@ class State {
             if (!removed) return;
 
             try {
-                const ret = options && options.revertIfAborted
-                    ? await this._revertibleTreeUpdate(taskCtx, params)
+                const ret = options && (options.revertIfAborted || options.revertOnError)
+                    ? await this._revertibleTreeUpdate(taskCtx, params, options)
                     : await this._updateTree(taskCtx, params);
                 return ret.cell;
             } finally {
@@ -156,10 +176,11 @@ class State {
 
     private updateQueue = new AsyncQueue<UpdateParams>();
 
-    private async _revertibleTreeUpdate(taskCtx: RuntimeContext, params: UpdateParams) {
+    private async _revertibleTreeUpdate(taskCtx: RuntimeContext, params: UpdateParams, options: Partial<State.UpdateOptions>) {
         const old = this.tree;
         const ret = await this._updateTree(taskCtx, params);
-        if (ret.ctx.wasAborted) return await this._updateTree(taskCtx, { tree: old, options: params.options });
+        let revert = ((ret.ctx.hadError || ret.ctx.wasAborted) && options.revertOnError) || (ret.ctx.wasAborted && options.revertIfAborted);
+        if (revert) return await this._updateTree(taskCtx, { tree: old, options: params.options });
         return ret;
     }
 
@@ -256,7 +277,8 @@ namespace State {
     export interface UpdateOptions {
         doNotLogTiming: boolean,
         doNotUpdateCurrent: boolean,
-        revertIfAborted: boolean
+        revertIfAborted: boolean,
+        revertOnError: boolean
     }
 
     export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) {
@@ -267,7 +289,8 @@ namespace State {
 const StateUpdateDefaultOptions: State.UpdateOptions = {
     doNotLogTiming: false,
     doNotUpdateCurrent: false,
-    revertIfAborted: false
+    revertIfAborted: false,
+    revertOnError: false
 };
 
 type Ref = StateTransform.Ref

+ 12 - 4
src/mol-util/data-source.ts

@@ -73,25 +73,33 @@ function isDone(data: XMLHttpRequest | FileReader) {
     throw new Error('unknown data type')
 }
 
+function genericError(isDownload: boolean) {
+    if (isDownload) return 'Failed to download data. Possible reasons: Resource is not available, or CORS is not allowed on the server.';
+    return 'Failed to open file.';
+}
+
 function readData<T extends XMLHttpRequest | FileReader>(ctx: RuntimeContext, action: string, data: T): Promise<T> {
-    return new Promise<T>((resolve, reject) => {
+    return new Promise<T>((resolve, reject) => {        
         // first check if data reading is already done
         if (isDone(data)) {
             const { error } = data as FileReader;
             if (error !== null && error !== undefined) {
-                reject(error ?? 'Failed.');
+                reject(error ?? genericError(data instanceof XMLHttpRequest));
             } else {
                 resolve(data);
             }
             return
         }
 
+        let hasError = false;
+
         data.onerror = (e: ProgressEvent) => {
+            if (hasError) return;
+
             const { error } = e.target as FileReader;
-            reject(error ?? 'Failed.');
+            reject(error ?? genericError(data instanceof XMLHttpRequest));
         };
 
-        let hasError = false;
         data.onprogress = (e: ProgressEvent) => {
             if (!ctx.shouldUpdate || hasError) return;