Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .yarn/versions/a6d19319.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
releases:
miew: patch
miew-app: patch
miew-react: patch
3 changes: 3 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ plugins:
spec: "@yarnpkg/plugin-version"

yarnPath: .yarn/releases/yarn-3.6.3.cjs

changesetBaseRefs:
- "origin/main"
5 changes: 5 additions & 0 deletions packages/miew/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Fixed

- Fix uncaught promise rejection when a load operation is cancelled. The `load()` promise now
resolves normally on cancellation instead of rejecting with an unhandled error.

## [0.12.0] - 2026-06-09

### Changed
Expand Down
35 changes: 23 additions & 12 deletions packages/miew/src/Miew.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Visual from './Visual';
import ComplexVisual from './ComplexVisual';
import Complex from './chem/Complex';
import VolumeVisual from './VolumeVisual';
import CancellationError from './io/CancellationError';
import io from './io/io';
import modes from './gfx/modes';
import colorers from './gfx/colorers';
Expand Down Expand Up @@ -1831,9 +1832,10 @@ function updateBinaryMode(opts) {
}

function _fetchData(source, opts, job) {
return new Promise(((resolve) => {
return new Promise(((resolve, reject) => {
if (job.shouldCancel()) {
throw new Error('Operation cancelled');
reject(new CancellationError('Operation cancelled'));
return;
}
job.notify({ type: 'fetching' });

Expand All @@ -1843,7 +1845,8 @@ function _fetchData(source, opts, job) {
// detect a proper loader
const TheLoader = _.head(io.loaders.find({ type: opts.sourceType, source }));
if (!TheLoader) {
throw new Error(LOADER_NOT_FOUND);
reject(new Error(LOADER_NOT_FOUND));
return;
}

// split file name
Expand Down Expand Up @@ -1898,11 +1901,13 @@ function _fetchData(source, opts, job) {
.catch((error) => {
console.timeEnd('fetch');
opts.context.logger.debug(error.message);
if (error.stack) {
opts.context.logger.debug(error.stack);
if (!(error instanceof CancellationError)) {
if (error.stack) {
opts.context.logger.debug(error.stack);
}
opts.context.logger.error('Fetching failed');
job.notify({ type: 'fetchingDone', error });
}
opts.context.logger.error('Fetching failed');
job.notify({ type: 'fetchingDone', error });
throw error;
});
resolve(promise);
Expand All @@ -1911,7 +1916,7 @@ function _fetchData(source, opts, job) {

function _parseData(data, opts, job) {
if (job.shouldCancel()) {
return Promise.reject(new Error('Operation cancelled'));
return Promise.reject(new CancellationError('Operation cancelled'));
}

job.notify({ type: 'parsing' });
Expand All @@ -1936,11 +1941,13 @@ function _parseData(data, opts, job) {
console.timeEnd('parse');
opts.error = error;
opts.context.logger.debug(error.message);
if (error.stack) {
opts.context.logger.debug(error.stack);
if (!(error instanceof CancellationError)) {
if (error.stack) {
opts.context.logger.debug(error.stack);
}
opts.context.logger.error('Parsing failed');
job.notify({ type: 'parsingDone', error });
}
opts.context.logger.error('Parsing failed');
job.notify({ type: 'parsingDone', error });
throw error;
});
}
Expand Down Expand Up @@ -2006,6 +2013,10 @@ Miew.prototype.load = function (source, opts) {
return onLoadEnd(name);
})
.catch((err) => {
if (err instanceof CancellationError) {
this.logger.debug('Loading cancelled');
return onLoadEnd(undefined);
}
this.logger.error('Could not load data');
this.logger.debug(err);
throw onLoadEnd(err);
Expand Down
7 changes: 7 additions & 0 deletions packages/miew/src/io/CancellationError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default class CancellationError extends Error {
constructor(message) {
super(message);
this.name = 'CancellationError';
Object.setPrototypeOf(this, new.target.prototype);
}
}
31 changes: 31 additions & 0 deletions packages/miew/src/io/CancellationError.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import chai, { expect } from 'chai';
import dirtyChai from 'dirty-chai';
import CancellationError from './CancellationError';

chai.use(dirtyChai);

describe('CancellationError', () => {
describe('constructor', () => {
let error;

beforeEach(() => {
error = new CancellationError('foo');
});

it('creates an Error', () => {
expect(error).to.be.an('error');
});

it('creates a throwable instance', () => {
expect(() => { throw error; }).to.throw(CancellationError);
});

it('sets correct name', () => {
expect(error).to.have.property('name', 'CancellationError');
});

it('sets correct message', () => {
expect(error).to.have.property('message', 'foo');
});
});
});
2 changes: 2 additions & 0 deletions packages/miew/src/io/io.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import CancellationError from './CancellationError';
import loaders from './loaders';
import parsers from './parsers';
import exporters from './exporters';

export default {
CancellationError,
loaders,
parsers,
exporters,
Expand Down
6 changes: 4 additions & 2 deletions packages/miew/src/io/loaders/FileLoader.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CancellationError from '../CancellationError';
import Loader from './Loader';

export default class FileLoader extends Loader {
Expand All @@ -11,7 +12,8 @@ export default class FileLoader extends Loader {
load() {
return new Promise((resolve, reject) => {
if (this._abort) {
throw new Error('Loading aborted');
reject(new CancellationError('Loading aborted'));
return;
}

const blob = this._source;
Expand All @@ -24,7 +26,7 @@ export default class FileLoader extends Loader {
reject(reader.error);
});
reader.addEventListener('abort', () => {
reject(new Error('Loading aborted'));
reject(new CancellationError('Loading aborted'));
});
reader.addEventListener('progress', (event) => {
this.dispatchEvent(event);
Expand Down
16 changes: 16 additions & 0 deletions packages/miew/src/io/loaders/FileLoader.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import chai, { expect } from 'chai';
import dirtyChai from 'dirty-chai';
import sinon from 'sinon';
import CancellationError from '../CancellationError';
import FileLoader from './FileLoader';

chai.use(dirtyChai);
Expand Down Expand Up @@ -117,12 +118,27 @@ describe('FileLoader', () => {
return expect(loader.load()).to.be.rejected();
});

it('rejects with CancellationError if aborted beforehand', () => {
loader.abort();
return expect(loader.load()).to.be.rejected().then((err) => {
expect(err).to.be.instanceOf(CancellationError);
});
});

it('rejects a promise if aborted afterwards', () => {
const promise = loader.load();
loader.abort();
return expect(promise).to.be.rejected();
});

it('rejects with CancellationError if aborted afterwards', () => {
const promise = loader.load();
loader.abort();
return expect(promise).to.be.rejected().then((err) => {
expect(err).to.be.instanceOf(CancellationError);
});
});

it('generates progress events', () => {
const onProgress = sinon.spy();
loader.addEventListener('progress', onProgress);
Expand Down
6 changes: 4 additions & 2 deletions packages/miew/src/io/loaders/XHRLoader.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import _ from 'lodash';
import CancellationError from '../CancellationError';
import Loader from './Loader';

// we don't need to detect all kinds of URLs, just the evident ones
Expand All @@ -15,7 +16,8 @@ export default class XHRLoader extends Loader {
load() {
return new Promise((resolve, reject) => {
if (this._abort) {
throw new Error('Loading aborted');
reject(new CancellationError('Loading aborted'));
return;
}

const url = this._source;
Expand All @@ -32,7 +34,7 @@ export default class XHRLoader extends Loader {
reject(new Error('HTTP request failed'));
});
request.addEventListener('abort', () => {
reject(new Error('Loading aborted'));
reject(new CancellationError('Loading aborted'));
});
request.addEventListener('progress', (event) => {
this.dispatchEvent(event);
Expand Down
16 changes: 16 additions & 0 deletions packages/miew/src/io/loaders/XHRLoader.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import chai, { expect } from 'chai';
import dirtyChai from 'dirty-chai';
import sinon from 'sinon';
import CancellationError from '../CancellationError';
import XHRLoader from './XHRLoader';

chai.use(dirtyChai);
Expand Down Expand Up @@ -115,12 +116,27 @@ describe('XHRLoader', () => {
return expect(loader.load()).to.be.rejected();
});

it('rejects with CancellationError if aborted beforehand', () => {
loader.abort();
return expect(loader.load()).to.be.rejected().then((err) => {
expect(err).to.be.instanceOf(CancellationError);
});
});

it('rejects a promise if aborted afterwards', () => {
const promise = loader.load();
loader.abort();
return expect(promise).to.be.rejected();
});

it('rejects with CancellationError if aborted afterwards', () => {
const promise = loader.load();
loader.abort();
return expect(promise).to.be.rejected().then((err) => {
expect(err).to.be.instanceOf(CancellationError);
});
});

it('generates progress events', () => {
const onProgress = sinon.spy();
loader.addEventListener('progress', onProgress);
Expand Down
3 changes: 2 additions & 1 deletion packages/miew/src/io/parsers/Parser.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CancellationError from '../CancellationError';
import makeContextDependent from '../../utils/makeContextDependent';

export default class Parser {
Expand All @@ -16,7 +17,7 @@ export default class Parser {
setTimeout(() => {
try {
if (this._abort) {
return reject(new Error('Parsing aborted'));
return reject(new CancellationError('Parsing aborted'));
}
return resolve(this.parseSync());
} catch (error) {
Expand Down
16 changes: 16 additions & 0 deletions packages/miew/src/io/parsers/Parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import chai, { expect } from 'chai';
import dirtyChai from 'dirty-chai';
import sinon from 'sinon';
import chaiAsPromised from 'chai-as-promised';
import CancellationError from '../CancellationError';
import Parser from './Parser';

chai.use(dirtyChai);
Expand Down Expand Up @@ -50,5 +51,20 @@ describe('Parser', () => {
expect(parser.parseSync).to.not.have.been.called();
});
});

it('rejects with CancellationError if aborted beforehand', () => {
parser.abort();
return expect(parser.parse()).to.be.rejected().then((err) => {
expect(err).to.be.instanceOf(CancellationError);
});
});

it('rejects with CancellationError if aborted afterwards', () => {
const promise = parser.parse();
parser.abort();
return expect(promise).to.be.rejected().then((err) => {
expect(err).to.be.instanceOf(CancellationError);
});
});
});
});
5 changes: 1 addition & 4 deletions packages/miew/src/io/parsers/ParsingError.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ class ParsingError extends Error {
constructor(message, line, column) {
super(`data:${line}:${column}: ${message}`);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, ParsingError);
}

this.name = 'ParsingError';
this.parseLine = line;
this.parseColumn = column;
Object.setPrototypeOf(this, new.target.prototype);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/miew/src/io/parsers/ParsingError.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('ParsingError', () => {
expect(error).to.be.an('error');
});

it('creates an throwable instance', () => {
it('creates a throwable instance', () => {
expect(() => { throw error; }).to.throw(ParsingError);
});

Expand Down
4 changes: 2 additions & 2 deletions packages/miew/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,9 @@ DebugTracer.spaces = '

class OutOfMemoryError extends Error {
constructor(message) {
super();
super(message);
this.name = 'OutOfMemoryError';
this.message = message;
Object.setPrototypeOf(this, new.target.prototype);
}
}

Expand Down
Loading
Loading