-
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 makeCancelable and pause functions (#64)
* feat(async): add makeCancelable and pause functions * style(lint): adjust complexity threshold in eslint config * test(async): add test case to cover standard promise rejection * chore: increase complexity threshold * chore: further adjust complexity thresholds * chore: add threshold to pause delay test * chore: add test for error handling post cancelation * chore: update comment formatting
- Loading branch information
1 parent
011fd91
commit 37132d0
Showing
10 changed files
with
146 additions
and
14 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
This file was deleted.
Oops, something went wrong.
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,15 @@ | ||
{ | ||
"parser": "@typescript-eslint/parser", | ||
"extends": [ | ||
"plugin:@typescript-eslint/recommended", | ||
"prettier/@typescript-eslint", | ||
"plugin:prettier/recommended" | ||
], | ||
"parserOptions": { | ||
"ecmaVersion": 2018, | ||
"sourceType": "module" | ||
}, | ||
"rules": { | ||
"complexity": [2, 10] | ||
} | ||
} |
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 |
---|---|---|
@@ -1,5 +1,6 @@ | ||
{ | ||
"semi": false, | ||
"singleQuote": true, | ||
"trailingComma": "all" | ||
} | ||
"trailingComma": "all", | ||
"printWidth": 100 | ||
} |
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,2 @@ | ||
export { default as makeCancelable } from './makeCancelable' | ||
export { default as pause } from './pause' |
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,40 @@ | ||
import makeCancelable, { canceledError } from './makeCancelable' | ||
import pause from './pause' | ||
|
||
describe('Cancel promise (makeCancelable)', () => { | ||
it('should resolve normally if not cancelled', async () => { | ||
const cancelablePromise = makeCancelable(pause(20)) | ||
const result = cancelablePromise.then(() => 'resolved') | ||
await expect(result).resolves.toEqual('resolved') | ||
}) | ||
|
||
it('should reject with a canceled error if cancel is called', async () => { | ||
const cancelablePromise = makeCancelable(pause(20)) | ||
const result = cancelablePromise.then(() => 'resolved') | ||
cancelablePromise.cancel() | ||
await expect(result).rejects.toEqual(canceledError) | ||
}) | ||
|
||
it('should allow errors to bubble up to the cancelable promise', async () => { | ||
const error = new Error('Error in bad promise') | ||
const badPromise = new Promise((resolve, reject) => setTimeout(() => reject(error), 50)) | ||
|
||
const cancelablePromise = makeCancelable(badPromise) | ||
const result = cancelablePromise.then(() => 'resolved') | ||
|
||
await expect(result).rejects.toEqual(error) | ||
}) | ||
|
||
it('should reject with a canceled error if cancel is called before the error occurs', async () => { | ||
const error = new Error('Error in bad promise') | ||
// Will throw an error in 50 ms | ||
const badPromise = new Promise((resolve, reject) => setTimeout(() => reject(error), 50)) | ||
|
||
const cancelablePromise = makeCancelable(badPromise) | ||
// Will cancel in 20 ms, before the error above throws | ||
setTimeout(() => cancelablePromise.cancel(), 20) | ||
const result = cancelablePromise.then(() => 'resolved') | ||
|
||
await expect(result).rejects.toEqual(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,53 @@ | ||
export const canceledError = { isCanceled: true } | ||
|
||
/** A promise that can have it's resolution cancelled */ | ||
export interface CancelablePromise<T> extends Promise<T> { | ||
cancel(): void | ||
} | ||
|
||
/** | ||
* | ||
* Allows the provided promise to be canceled after starting. This does not stop the promise from executing but will | ||
* cause it to reject with the value `{ isCanceled: true }` once it finishes, regardless of outcome. | ||
* | ||
* @param promise The promise that is executing | ||
* @return The cancelable version of the promise | ||
* | ||
* @example | ||
* const promise = new Promise((res, rej) => { | ||
* setTimeout(() => res('I finished!'), 3000) | ||
* }) | ||
* | ||
* // Create a cancelable version of the promise | ||
* const cancelablePromise = makeCancelable(promise) | ||
* | ||
* // Stop the cancelable promise from resolving | ||
* cancelablePromise.cancel() | ||
* | ||
* promise | ||
* .then(result => console.log('Normal', result)) // This will log `'I finished!'` after 3000ms | ||
* .catch(err => console.log('Normal', err)) // Will reject as per normal | ||
* | ||
* 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 }` | ||
* | ||
* @category async | ||
* | ||
*/ | ||
export default function makeCancelable<T>(promise: Promise<T>): CancelablePromise<T> { | ||
let hasCanceled = false | ||
|
||
const cancelablePromise: Partial<CancelablePromise<T>> = new Promise((resolve, reject) => { | ||
promise.then( | ||
val => (hasCanceled ? reject(canceledError) : resolve(val)), | ||
error => (hasCanceled ? reject(canceledError) : reject(error)), | ||
) | ||
}) | ||
|
||
cancelablePromise.cancel = () => { | ||
hasCanceled = true | ||
} | ||
|
||
return cancelablePromise as CancelablePromise<T> | ||
} |
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,10 @@ | ||
import pause from './pause' | ||
|
||
describe('Wait for delay (pause)', () => { | ||
it('should resolve after a minimum of the provided ms', async () => { | ||
const delay = 50 | ||
const start = Date.now() | ||
await pause(delay) | ||
expect(Date.now() - start).toBeGreaterThanOrEqual(delay - 5) // 5 ms threshold for random CPU variance | ||
}) | ||
}) |
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,15 @@ | ||
/** | ||
* | ||
* Creates a promise that resolves in the provided number of milliseconds. | ||
* | ||
* This function is basically a promise version of `setTimeout` | ||
* | ||
* @param ms The number of ms to pause for | ||
* @return The executing promise | ||
* | ||
* @category async | ||
* | ||
*/ | ||
export default function wait(ms: number) { | ||
return new Promise(resolve => setTimeout(resolve, ms)) | ||
} |
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