-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(async): add CancelablePromise utility class
- Loading branch information
1 parent
17def6f
commit 3f7f1f6
Showing
4 changed files
with
143 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { CancelablePromise, canceledError } from './CancelablePromise' | ||
|
||
describe('CancelablePromise', () => { | ||
const createPromise = () => | ||
new CancelablePromise(resolve => setTimeout(resolve, 0, 'hello world')) | ||
|
||
const createFailingPromise = () => | ||
new CancelablePromise((_, reject) => setTimeout(reject, 0, 'some error')) | ||
|
||
const createMockFns = () => ({ | ||
onFulfilled: jest.fn(val => val), | ||
onRejected: jest.fn(val => val), | ||
onFinally: jest.fn(), | ||
}) | ||
|
||
it('resolves like a regular promise when not canceled', async () => { | ||
const cancelable = createPromise() | ||
const { onFulfilled, onRejected } = createMockFns() | ||
cancelable.then(onFulfilled).catch(onRejected) | ||
const value = await cancelable | ||
expect(value).toBe('hello world') | ||
expect(onFulfilled).toHaveBeenCalledWith('hello world') | ||
expect(onRejected).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('rejects like a regular promise when not canceled', async () => { | ||
const cancelable = createFailingPromise() | ||
const { onFulfilled, onRejected } = createMockFns() | ||
await cancelable.catch(onRejected) | ||
expect(onFulfilled).not.toHaveBeenCalled() | ||
expect(onRejected).toHaveBeenCalledWith('some error') | ||
}) | ||
|
||
it('completes like a regular promise when not canceled', async () => { | ||
const cancelable = createPromise() | ||
const { onFulfilled, onRejected, onFinally } = createMockFns() | ||
await cancelable.then(onFulfilled) | ||
await cancelable.catch(onRejected) | ||
await cancelable.finally(onFinally) | ||
expect(onFulfilled).toHaveBeenCalledWith('hello world') | ||
expect(onRejected).not.toHaveBeenCalled() | ||
expect(onFinally).toHaveBeenCalled() | ||
}) | ||
|
||
it('stringifies to the string [object CancelablePromise]', () => { | ||
const cancelable = createPromise() | ||
expect(`${cancelable}`).toEqual('[object CancelablePromise]') | ||
}) | ||
|
||
it('rejects with the constant cancelable error when canceled', async () => { | ||
const cancelable = createPromise() | ||
const { onFulfilled, onRejected } = createMockFns() | ||
const check = cancelable.then(onFulfilled).catch(onRejected) | ||
cancelable.cancel() | ||
await check | ||
expect(onFulfilled).not.toHaveBeenCalled() | ||
expect(onRejected).toHaveBeenCalledWith(canceledError) | ||
}) | ||
|
||
it('rejects with the constant cancelable error when canceled, even if the promise failed', async () => { | ||
const cancelable = createFailingPromise() | ||
const { onFulfilled, onRejected } = createMockFns() | ||
const check = cancelable.then(onFulfilled).catch(onRejected) | ||
cancelable.cancel() | ||
await check | ||
expect(onFulfilled).not.toHaveBeenCalled() | ||
expect(onRejected).toHaveBeenCalledWith(canceledError) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/** @module async */ | ||
/** */ | ||
|
||
import { canceledError } from './makeCancelable' | ||
|
||
export { canceledError } | ||
|
||
/** | ||
* | ||
* Creates a promise that can be canceled after starting. Canceling the promise does not stop it from executing but will | ||
* cause it to reject with the value `{ isCanceled: true }` once it finishes, regardless of outcome. | ||
* | ||
* ```ts | ||
* | ||
* const promise = new CancelablePromise(res => setTimeout(res, 3000, 'I finished!')) | ||
* | ||
* // Stop the cancelable promise from resolving | ||
* cancelablePromise.cancel() | ||
* | ||
* cancelablePromise | ||
* .then(result => console.log('Cancelable', result)) // Never fires, the promise will not resolve after being cancelled | ||
* .catch(err => console.log('Cancelable', err)) // Resolves after 3000ms with the value `{ isCanceled: true }` | ||
* ``` | ||
* | ||
*/ | ||
export class CancelablePromise<T extends any> extends Promise<T> { | ||
private canceled = false | ||
protected promise: Promise<T> | ||
|
||
get [Symbol.toStringTag]() { | ||
return 'CancelablePromise' | ||
} | ||
|
||
get hasCanceled() { | ||
return this.canceled | ||
} | ||
|
||
constructor(executor: (resolve: (value: T) => void, reject: (err?: any) => void) => void) { | ||
super((resolve, reject) => { | ||
return new Promise(executor).then( | ||
(val: T) => (this.hasCanceled ? reject(canceledError) : resolve(val)), | ||
error => (this.hasCanceled ? reject(canceledError) : reject(error)), | ||
) | ||
}) | ||
} | ||
|
||
cancel() { | ||
this.canceled = true | ||
} | ||
|
||
then<TResult1, TResult2>( | ||
onfulfilled: (value: T) => TResult1 | PromiseLike<TResult1>, | ||
onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>, | ||
): Promise<TResult1 | TResult2> { | ||
return super.then(onfulfilled, onrejected) | ||
} | ||
|
||
catch<TResult>( | ||
onrejected: (reason: any) => TResult | PromiseLike<TResult>, | ||
): Promise<T | TResult> { | ||
return super.catch(onrejected) | ||
} | ||
|
||
finally(onfinally: () => void): Promise<T> { | ||
return super.finally(onfinally) | ||
} | ||
} | ||
|
||
export default CancelablePromise |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters