diff --git a/src/mol-plugin-state/actions/structure.ts b/src/mol-plugin-state/actions/structure.ts index 1d4e898d06239ca200a4ac4676c1ee1c2da30d7f..8406a926910f501cdf949f663f11d6882dd1b9da 100644 --- a/src/mol-plugin-state/actions/structure.ts +++ b/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>[] { diff --git a/src/mol-plugin-state/builder/data.ts b/src/mol-plugin-state/builder/data.ts index fbd224c12a5246284075e577c1e14055cd60ae1f..99143b1f903538537bbdf27260a85899cee1b722 100644 --- a/src/mol-plugin-state/builder/data.ts +++ b/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 }; } diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index de17fbbb7dc8d8ac5f762565a512596b60fbf6fd..b730b36478cf957e0d72e951d50985d63e5785d5 100644 --- a/src/mol-state/state.ts +++ b/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 diff --git a/src/mol-util/data-source.ts b/src/mol-util/data-source.ts index 88c405bd3e137301c3259dd14250c6cfecda2cab..402a023df4e5329ec80927a439be61e49b6ca370 100644 --- a/src/mol-util/data-source.ts +++ b/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;