diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index 074d1a8b4..0450358ab 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -11,6 +11,7 @@ import writeScript from './writeScript.js'; import DicomLoaderService from './dicomLoaderService.js'; import b64toBlob from './b64toBlob.js'; import * as urlUtil from './urlUtil'; +import makeCancelable from './makeCancelable'; const utils = { guid, @@ -27,6 +28,7 @@ const utils = { DICOMTagDescriptions, DicomLoaderService, urlUtil, + makeCancelable, }; export { @@ -44,6 +46,7 @@ export { DICOMTagDescriptions, DicomLoaderService, urlUtil, + makeCancelable, }; export default utils; diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index c1788b5be..7742dcebd 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -17,6 +17,7 @@ describe('Top level exports', () => { 'DICOMTagDescriptions', 'DicomLoaderService', 'urlUtil', + 'makeCancelable', ].sort(); const exports = Object.keys(utils.default).sort(); diff --git a/platform/core/src/utils/makeCancelable.js b/platform/core/src/utils/makeCancelable.js new file mode 100644 index 000000000..11e507103 --- /dev/null +++ b/platform/core/src/utils/makeCancelable.js @@ -0,0 +1,19 @@ +export default function makeCancelable(thenable) { + let isCanceled = false; + const promise = Promise.resolve(thenable).then( + function(result) { + if (isCanceled) throw Object.freeze({ isCanceled }); + return result; + }, + function(error) { + if (isCanceled) throw Object.freeze({ isCanceled, error }); + throw error; + } + ); + return Object.assign(Object.create(promise), { + then: promise.then.bind(promise), + cancel() { + isCanceled = true; + }, + }); +} diff --git a/platform/ui/src/components/studyBrowser/ImageThumbnail.js b/platform/ui/src/components/studyBrowser/ImageThumbnail.js index 01b99c9c4..c80855232 100644 --- a/platform/ui/src/components/studyBrowser/ImageThumbnail.js +++ b/platform/ui/src/components/studyBrowser/ImageThumbnail.js @@ -1,6 +1,7 @@ /* global cornerstone */ import './ImageThumbnail.styl'; +import { utils } from '@ohif/core'; import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; @@ -32,7 +33,7 @@ export default class ImageThumbnail extends PureComponent { constructor(props) { super(props); - + this.cancelablePromises = []; this.canvas = React.createRef(); this.state = { loading: this.shouldRenderToCanvas(), @@ -43,10 +44,17 @@ export default class ImageThumbnail extends PureComponent { return this.props.imageId && !this.props.imageSrc; } + fetchImage() { + const cancelablePromise = utils.makeCancelable( + cornerstone.loadAndCacheImage(this.props.imageId) + ); + this.cancelablePromises.push(cancelablePromise); + return cancelablePromise; + } + componentDidMount() { if (this.shouldRenderToCanvas()) { - cornerstone - .loadAndCacheImage(this.props.imageId) + this.fetchImage() .then(image => { cornerstone.renderToCanvas(this.canvas.current, image); this.setState({ @@ -54,6 +62,7 @@ export default class ImageThumbnail extends PureComponent { }); }) .catch(error => { + if (error.isCanceled) return; this.setState({ loading: false, error: true, @@ -63,6 +72,12 @@ export default class ImageThumbnail extends PureComponent { } } + componentWillUnmount() { + while (this.cancelablePromises.length > 0) { + this.cancelablePromises.pop().cancel(); + } + } + render() { let loadingOrError; if (this.props.error) { diff --git a/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js b/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js index a61c3269a..1fe47c813 100644 --- a/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js +++ b/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import { extensionManager } from './../App.js'; const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata; -const { retrieveStudiesMetadata } = studies; +const { retrieveStudiesMetadata, deleteStudyMetadataPromise } = studies; const { studyMetadataManager, updateMetaDataManager } = utils; class ViewerRetrieveStudyData extends Component { @@ -18,6 +18,8 @@ class ViewerRetrieveStudyData extends Component { constructor(props) { super(props); + this.abortSeriesLoad = false; + this.seriesLoadStats = Object.create(null); this.state = { studies: null, error: null, @@ -83,18 +85,49 @@ class ViewerRetrieveStudyData extends Component { }); } + _handleSeriesLoadResult(error, studyMetadata, series) { + if (this.abortSeriesLoad) return; + const stats = this.seriesLoadStats[studyMetadata.getStudyInstanceUID()]; + if (!stats) return; + stats.count--; + if (error || !series) { + stats.errors++; + log.error(error || 'Bad Series'); + return; + } + this._addSeriesToStudy(studyMetadata, series); + } + _attemptToLoadRemainingSeries(studyMetadata) { const { seriesLoader } = studyMetadata.getData(); if (!seriesLoader) { return; } + const stats = (this.seriesLoadStats[studyMetadata.getStudyInstanceUID()] = { + errors: 0, + count: 0, + }); while (seriesLoader.hasNext()) { seriesLoader .next() .then( - series => void this._addSeriesToStudy(studyMetadata, series), - error => void log.error(error) + series => + void this._handleSeriesLoadResult(null, studyMetadata, series), + error => void this._handleSeriesLoadResult({ error }, null, null) ); + stats.count++; + } + } + + componentWillUnmount() { + this.abortSeriesLoad = true; + for (const studyInstanceUid in this.seriesLoadStats) { + const stats = this.seriesLoadStats[studyInstanceUid]; + if (stats && (stats.count > 0 || stats.errors > 0)) { + deleteStudyMetadataPromise(studyInstanceUid); + studyMetadataManager.remove(studyInstanceUid); + log.info(`Purging incomplete study data: ${studyInstanceUid}`); + } } }