Skip to content
Merged
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
50 changes: 49 additions & 1 deletion eclipse-scout-core/src/util/promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
import {objects, PromiseCreator} from '../index';
import {Abortable, objects, PromiseCreator} from '../index';
import $ from 'jquery';

export const promises = {
Expand Down Expand Up @@ -179,3 +179,51 @@ export class Deferred<T> {
return this._promise;
}
}

/**
* A helper class that makes ES6 promises abortable. The {@link AbortablePromise} is itself a ES6 promise and can be used similarly.
* When it is aborted is will be rejected with a {@link AbortError}.
*/
export class AbortablePromise<T> extends Promise<T> implements Abortable {

protected _reject: (reason: any) => void;

constructor(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void) {
let _reject: (reason: any) => void;

super((resolve, reject) => {
_reject = reject;
executor(resolve, reject);
});

this._reject = _reject;
}

/**
* Creates an {@link AbortablePromise} from the given ES6 promise.
*/
static of<T>(promise: Promise<T>): AbortablePromise<T> {
if (!promise) {
return;
}
return new AbortablePromise((resolve, reject) => promise.then(resolve, reject));
}

/**
* Aborts this promise with a {@link AbortError}.
*/
abort() {
this._reject(new AbortError());
}
}

/**
* Marker class for aborted promises.
*/
export class AbortError {
objectType: string;

constructor() {
this.objectType = 'AbortError';
}
}
108 changes: 106 additions & 2 deletions eclipse-scout-core/test/util/promisesSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
* AI Disclosure: This file was partially AI-generated.
* The AI-generated portions are made available under CC0-1.0
* and not subject to the project's licence.
*
* SPDX-License-Identifier: EPL-2.0 and CC0-1.0
*/
import {arrays, PromiseCreator, promises} from '../../src/index';
import {AbortablePromise, AbortError, arrays, Deferred, PromiseCreator, promises} from '../../src/index';

describe('promises', () => {

Expand Down Expand Up @@ -163,4 +167,104 @@ describe('promises', () => {
await expectAsync(promises.ensure('3')).toBeResolvedTo('3');
});
});

describe('AbortablePromise', () => {

describe('constructor', () => {

it('resolves when executor calls resolve', async () => {
const abortablePromise = new AbortablePromise<number>(resolve => resolve(42));
await expectAsync(abortablePromise).toBeResolvedTo(42);
});

it('rejects when executor calls reject', async () => {
const abortablePromise = new AbortablePromise<number>((resolve, reject) => reject('error'));
await expectAsync(abortablePromise).toBeRejectedWith('error');
});

it('is a native Promise', () => {
const abortablePromise = new AbortablePromise<void>(resolve => resolve());
expect(abortablePromise).toBeInstanceOf(Promise);
});
});

describe('abort', () => {

it('rejects the promise with AbortError', async () => {
const abortablePromise = new AbortablePromise<void>(() => {
});
abortablePromise.abort();
await expectAsync(abortablePromise).toBeRejectedWith(new AbortError());
});

it('has no effect after the promise has already resolved', async () => {
const abortablePromise = new AbortablePromise<string>(resolve => resolve('done'));
await expectAsync(abortablePromise).toBeResolvedTo('done');
abortablePromise.abort();
await expectAsync(abortablePromise).toBeResolvedTo('done');
});

it('has no effect after the promise has already rejected', async () => {
const abortablePromise = new AbortablePromise<void>((resolve, reject) => reject('fail'));
await expectAsync(abortablePromise).toBeRejectedWith('fail');
abortablePromise.abort();
await expectAsync(abortablePromise).toBeRejectedWith('fail');
});

it('can be called multiple times without throwing', async () => {
const abortablePromise = new AbortablePromise<void>(() => {
});
abortablePromise.abort();
abortablePromise.abort();
await expectAsync(abortablePromise).toBeRejectedWith(new AbortError());
});
});

describe('of', () => {

it('returns undefined for a falsy argument', () => {
expect(AbortablePromise.of(null)).toBeUndefined();
expect(AbortablePromise.of(undefined)).toBeUndefined();
});

it('returns an AbortablePromise instance', () => {
const abortablePromise = AbortablePromise.of(Promise.resolve());
expect(abortablePromise).toBeInstanceOf(AbortablePromise);
});

it('wraps a resolved native Promise', async () => {
const abortablePromise = AbortablePromise.of(Promise.resolve('hello'));
await expectAsync(abortablePromise).toBeResolvedTo('hello');
});

it('wraps a rejected native Promise', async () => {
const abortablePromise = AbortablePromise.of(Promise.reject('boom'));
await expectAsync(abortablePromise).toBeRejectedWith('boom');
});

it('is resolved when the wrapped native Promise is resolved', async () => {
const deferred = new Deferred();
const abortablePromise = AbortablePromise.of(deferred.promise());
deferred.resolve('hello');
await expectAsync(abortablePromise).toBeResolvedTo('hello');
});

it('is rejected when the wrapped native Promise is rejected', async () => {
const deferred = new Deferred();
const abortablePromise = AbortablePromise.of(deferred.promise());
deferred.reject('boom');
await expectAsync(abortablePromise).toBeRejectedWith('boom');
});

it('aborting does not affect the original promise', async () => {
const deferred = new Deferred<string>();
const abortablePromise = AbortablePromise.of(deferred.promise());
abortablePromise.abort();
deferred.resolve('original');
// the original promise still resolves normally
await expectAsync(deferred.promise()).toBeResolvedTo('original');
await expectAsync(abortablePromise).toBeRejectedWith(new AbortError());
});
});
});
});
Loading