real-cancellable-promise
Advanced tools
Comparing version 1.1.2 to 1.2.0
@@ -0,1 +1,7 @@ | ||
## 1.2.0 | ||
### Chores | ||
- Update the signature of `CancellablePromise.race` to match that of `Promise.race` in the latest version of TypeScript. This should not be a breaking change for the vast majority of users. (#8) | ||
## 1.1.2 | ||
@@ -5,4 +11,4 @@ | ||
- Make the `capture` function of `buildCancellablePromise` an identity function | ||
from a type perspective. | ||
- Make the `capture` function of `buildCancellablePromise` an identity function | ||
from a type perspective. | ||
@@ -13,3 +19,3 @@ ## 1.1.1 | ||
- Fix `CancellablePromise<T>` not being assignable to `Promise<T>` | ||
- Fix `CancellablePromise<T>` not being assignable to `Promise<T>` | ||
@@ -20,3 +26,3 @@ ## 1.1.0 | ||
- Publish ES module | ||
- Publish ES module | ||
@@ -23,0 +29,0 @@ ## 1.0.0 |
/** | ||
* The most abstract thing we can cancel — a thenable with a cancel method. | ||
*/ | ||
export declare type PromiseWithCancel<T> = PromiseLike<T> & { | ||
export type PromiseWithCancel<T> = PromiseLike<T> & { | ||
cancel(): void; | ||
@@ -269,3 +269,3 @@ }; | ||
*/ | ||
static race<T>(values: readonly T[]): CancellablePromise<T extends PromiseLike<infer U> ? U : T>; | ||
static race<T extends readonly unknown[] | []>(values: T): CancellablePromise<Awaited<T[number]>>; | ||
/** | ||
@@ -272,0 +272,0 @@ * @returns a `CancellablePromise` that resolves after `ms` milliseconds. |
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
/** | ||
@@ -90,9 +88,10 @@ * If canceled, a [[`CancellablePromise`]] should throw an `Cancellation` object. | ||
} | ||
/* eslint-disable @typescript-eslint/no-explicit-any -- to match the types used for Promise in the official lib.d.ts */ | ||
/** | ||
* Analogous to `Promise.catch`. | ||
*/ | ||
catch(onRejected // eslint-disable-line @typescript-eslint/no-explicit-any -- to match the types used for Promise in the official lib.d.ts | ||
) { | ||
catch(onRejected) { | ||
return this.then(undefined, onRejected); | ||
} | ||
/* eslint-enable @typescript-eslint/no-explicit-any */ | ||
/** | ||
@@ -99,0 +98,0 @@ * Attaches a callback that is invoked when the Promise is settled |
@@ -14,3 +14,3 @@ import { CancellablePromise } from './CancellablePromise'; | ||
*/ | ||
export declare type CaptureCancellablePromise = <P extends CancellablePromise<unknown>>(promise: P) => P; | ||
export type CaptureCancellablePromise = <P extends CancellablePromise<unknown>>(promise: P) => P; | ||
/** | ||
@@ -17,0 +17,0 @@ * Used to build a single [[`CancellablePromise`]] from a multi-step asynchronous |
{ | ||
"name": "real-cancellable-promise", | ||
"version": "1.1.2", | ||
"version": "1.2.0", | ||
"description": "A simple cancellable promise implementation that cancels the underlying HTTP call.", | ||
@@ -25,6 +25,5 @@ "keywords": [ | ||
"scripts": { | ||
"build": "yarn clean && rollup -c .config/rollup.config.js", | ||
"build": "yarn clean && rollup -c .config/rollup.config.mjs", | ||
"clean": "rimraf dist", | ||
"lint": "eslint", | ||
"lint-all": "yarn lint .", | ||
"lint": "eslint .", | ||
"lint-staged": "lint-staged --no-stash", | ||
@@ -45,27 +44,28 @@ "prepack": "yarn build", | ||
"devDependencies": { | ||
"@rollup/plugin-typescript": "^8.2.5", | ||
"@swc/cli": "^0.1.51", | ||
"@swc/core": "^1.2.92", | ||
"@swc/jest": "^0.2.4", | ||
"@types/jest": "^27.0.2", | ||
"@typescript-eslint/eslint-plugin": "^4.32.0", | ||
"@typescript-eslint/parser": "^4.32.0", | ||
"eslint": "^7.32.0", | ||
"eslint-config-airbnb-base": "^14.2.1", | ||
"eslint-config-airbnb-typescript": "^14.0.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-import": "^2.24.2", | ||
"eslint-plugin-jest": "^25.0.1", | ||
"eslint-plugin-promise": "^5.1.0", | ||
"husky": "^7.0.2", | ||
"jest": "^27.2.4", | ||
"lint-staged": "^11.1.2", | ||
"prettier": "^2.4.1", | ||
"rimraf": "^3.0.2", | ||
"rollup": "^2.57.0", | ||
"tslib": "^2.3.1", | ||
"typedoc": "^0.22.4", | ||
"typescript": "^4.4.3" | ||
"@rollup/plugin-typescript": "^11.1.0", | ||
"@swc/cli": "^0.1.62", | ||
"@swc/core": "^1.3.50", | ||
"@swc/jest": "^0.2.24", | ||
"@types/jest": "^29.5.0", | ||
"@typescript-eslint/eslint-plugin": "^5.58.0", | ||
"@typescript-eslint/parser": "^5.58.0", | ||
"eslint": "^8.38.0", | ||
"eslint-config-airbnb-base": "^15.0.0", | ||
"eslint-config-airbnb-typescript": "^17.0.0", | ||
"eslint-config-prettier": "^8.8.0", | ||
"eslint-plugin-import": "^2.27.5", | ||
"eslint-plugin-jest": "^27.2.1", | ||
"eslint-plugin-promise": "^6.1.1", | ||
"husky": "^8.0.3", | ||
"jest": "^29.5.0", | ||
"lint-staged": "^13.2.1", | ||
"prettier": "^2.8.7", | ||
"prettier-plugin-packagejson": "^2.4.3", | ||
"rimraf": "^5.0.0", | ||
"rollup": "^3.20.2", | ||
"tslib": "^2.5.0", | ||
"typedoc": "^0.24.1", | ||
"typescript": "^5.0.4" | ||
}, | ||
"packageManager": "yarn@3.3.1" | ||
"packageManager": "yarn@3.5.0" | ||
} |
288
README.md
@@ -7,12 +7,12 @@ # real-cancellable-promise | ||
- ⚡ Compatible with [fetch](#fetch), [axios](#axios), and | ||
[jQuery.ajax](#jQuery) | ||
- 🐦 Lightweight — zero dependencies and less than 1 kB minified and gzipped | ||
- 🏭 Used in production by [Interface | ||
Technologies](http://www.iticentral.com/) | ||
- 💻 Optimized for TypeScript | ||
- ⚛ Built with React in mind | ||
- 🔎 Compatible with | ||
[react-query](https://react-query.tanstack.com/guides/query-cancellation) | ||
query cancellation out of the box | ||
- ⚡ Compatible with [fetch](#fetch), [axios](#axios), and | ||
[jQuery.ajax](#jQuery) | ||
- 🐦 Lightweight — zero dependencies and less than 1 kB minified and gzipped | ||
- 🏭 Used in production by [Interface | ||
Technologies](http://www.iticentral.com/) | ||
- 💻 Optimized for TypeScript | ||
- ⚛ Built with React in mind | ||
- 🔎 Compatible with | ||
[react-query](https://react-query.tanstack.com/guides/query-cancellation) | ||
query cancellation out of the box | ||
@@ -26,9 +26,9 @@ # The Basics | ||
```ts | ||
import { CancellablePromise } from 'real-cancellable-promise' | ||
import { CancellablePromise } from 'real-cancellable-promise'; | ||
const cancellablePromise = new CancellablePromise(normalPromise, cancel) | ||
const cancellablePromise = new CancellablePromise(normalPromise, cancel); | ||
cancellablePromise.cancel() | ||
cancellablePromise.cancel(); | ||
await cancellablePromise // throws a Cancellation object that subclasses Error | ||
await cancellablePromise; // throws a Cancellation object that subclasses Error | ||
``` | ||
@@ -44,3 +44,3 @@ | ||
```ts | ||
new CancellablePromise(normalPromise, () => {}) | ||
new CancellablePromise(normalPromise, () => {}); | ||
``` | ||
@@ -56,20 +56,20 @@ | ||
export function cancellableFetch( | ||
input: RequestInfo, | ||
init: RequestInit = {} | ||
input: RequestInfo, | ||
init: RequestInit = {} | ||
): CancellablePromise<Response> { | ||
const controller = new AbortController() | ||
const controller = new AbortController(); | ||
const promise = fetch(input, { | ||
...init, | ||
signal: controller.signal, | ||
}).catch((e) => { | ||
if (e.name === 'AbortError') { | ||
throw new Cancellation() | ||
} | ||
const promise = fetch(input, { | ||
...init, | ||
signal: controller.signal, | ||
}).catch((e) => { | ||
if (e.name === 'AbortError') { | ||
throw new Cancellation(); | ||
} | ||
// rethrow the original error | ||
throw e | ||
}) | ||
// rethrow the original error | ||
throw e; | ||
}); | ||
return new CancellablePromise<Response>(promise, () => controller.abort()) | ||
return new CancellablePromise<Response>(promise, () => controller.abort()); | ||
} | ||
@@ -79,4 +79,4 @@ | ||
const cancellablePromise = cancellableFetch(url, { | ||
/* pass options here */ | ||
}) | ||
/* pass options here */ | ||
}); | ||
``` | ||
@@ -89,33 +89,33 @@ | ||
export function cancellableFetch<T>( | ||
input: RequestInfo, | ||
init: RequestInit = {} | ||
input: RequestInfo, | ||
init: RequestInit = {} | ||
): CancellablePromise<T> { | ||
const controller = new AbortController() | ||
const controller = new AbortController(); | ||
const promise = fetch(input, { | ||
...init, | ||
signal: controller.signal, | ||
const promise = fetch(input, { | ||
...init, | ||
signal: controller.signal, | ||
}) | ||
.then((response) => { | ||
// Handle the response object however you want | ||
if (!response.ok) { | ||
throw new Error(`Fetch failed with status code ${response.status}.`); | ||
} | ||
if (response.headers.get('content-type')?.includes('application/json')) { | ||
return response.json(); | ||
} else { | ||
return response.text(); | ||
} | ||
}) | ||
.then((response) => { | ||
// Handle the response object however you want | ||
if (!response.ok) { | ||
throw new Error(`Fetch failed with status code ${response.status}.`) | ||
} | ||
.catch((e) => { | ||
if (e.name === 'AbortError') { | ||
throw new Cancellation(); | ||
} | ||
if (response.headers.get('content-type')?.includes('application/json')) { | ||
return response.json() | ||
} else { | ||
return response.text() | ||
} | ||
}) | ||
.catch((e) => { | ||
if (e.name === 'AbortError') { | ||
throw new Cancellation() | ||
} | ||
// rethrow the original error | ||
throw e; | ||
}); | ||
// rethrow the original error | ||
throw e | ||
}) | ||
return new CancellablePromise<T>(promise, () => controller.abort()) | ||
return new CancellablePromise<T>(promise, () => controller.abort()); | ||
} | ||
@@ -129,22 +129,24 @@ ``` | ||
```ts | ||
export function cancellableAxios<T>(config: AxiosRequestConfig): CancellablePromise<T> { | ||
const source = axios.CancelToken.source() | ||
config = { ...config, cancelToken: source.token } | ||
export function cancellableAxios<T>( | ||
config: AxiosRequestConfig | ||
): CancellablePromise<T> { | ||
const source = axios.CancelToken.source(); | ||
config = { ...config, cancelToken: source.token }; | ||
const promise = axios(config) | ||
.then((response) => response.data) | ||
.catch((e) => { | ||
if (e instanceof axios.Cancel) { | ||
throw new Cancellation() | ||
} | ||
const promise = axios(config) | ||
.then((response) => response.data) | ||
.catch((e) => { | ||
if (e instanceof axios.Cancel) { | ||
throw new Cancellation(); | ||
} | ||
// rethrow the original error | ||
throw e | ||
}) | ||
// rethrow the original error | ||
throw e; | ||
}); | ||
return new CancellablePromise<T>(promise, () => source.cancel()) | ||
return new CancellablePromise<T>(promise, () => source.cancel()); | ||
} | ||
// Use just like normal axios: | ||
const cancellablePromise = cancellableAxios({ url }) | ||
const cancellablePromise = cancellableAxios({ url }); | ||
``` | ||
@@ -156,18 +158,18 @@ | ||
export function cancellableJQueryAjax<T>( | ||
settings: JQuery.AjaxSettings | ||
settings: JQuery.AjaxSettings | ||
): CancellablePromise<T> { | ||
const xhr = $.ajax(settings) | ||
const xhr = $.ajax(settings); | ||
const promise = xhr.catch((e) => { | ||
if (e.statusText === 'abort') throw new Cancellation() | ||
const promise = xhr.catch((e) => { | ||
if (e.statusText === 'abort') throw new Cancellation(); | ||
// rethrow the original error | ||
throw e | ||
}) | ||
// rethrow the original error | ||
throw e; | ||
}); | ||
return new CancellablePromise<T>(promise, () => xhr.abort()) | ||
return new CancellablePromise<T>(promise, () => xhr.abort()); | ||
} | ||
// Use just like normal $.ajax: | ||
const cancellablePromise = cancellableJQueryAjax({ url, dataType: 'json' }) | ||
const cancellablePromise = cancellableJQueryAjax({ url, dataType: 'json' }); | ||
``` | ||
@@ -186,35 +188,35 @@ | ||
## React: Prevent setState after unmount | ||
## React: Cancel the API call when the component unmounts | ||
React will give you a warning if you attempt to update a component's state after | ||
it has unmounted. This will happen if your component makes an API call but gets | ||
unmounted before the API call completes. | ||
If your React component makes an API call, you probably don't care about the result of that API call after the component has unmounted. You can cancel the API in the cleanup function of an effect like this: | ||
You can fix this by canceling the API call in the cleanup function of an effect. | ||
```tsx | ||
function listBlogPosts(): CancellablePromise<Post[]> { | ||
// call the API | ||
// call the API | ||
} | ||
export function Blog() { | ||
const [posts, setPosts] = useState<Post[]>([]) | ||
const [posts, setPosts] = useState<Post[]>([]); | ||
useEffect(() => { | ||
const cancellablePromise = listBlogPosts().then(setPosts).catch(console.error) | ||
useEffect(() => { | ||
const cancellablePromise = listBlogPosts() | ||
.then(setPosts) | ||
.catch(console.error); | ||
// The promise will get canceled when the component unmounts | ||
return cancellablePromise.cancel | ||
}, []) | ||
// The promise will get canceled when the component unmounts | ||
return cancellablePromise.cancel; | ||
}, []); | ||
return ( | ||
<div> | ||
{posts.map((p) => { | ||
/* ... */ | ||
})} | ||
</div> | ||
) | ||
return ( | ||
<div> | ||
{posts.map((p) => { | ||
/* ... */ | ||
})} | ||
</div> | ||
); | ||
} | ||
``` | ||
Before React 18, this was necessary to prevent the infamous "setState after unmount" warning. This warning was removed from React in React 18 because setting state after the component unmounts is usually not indicative of a real problem. | ||
[CodeSandbox: prevent setState after | ||
@@ -225,33 +227,33 @@ unmount](https://codesandbox.io/s/real-cancellable-promise-prevent-setstate-after-unmount-2zqb0?file=/src/App.tsx) | ||
Sometimes API calls have parameters, like a search string entered by the user. | ||
Sometimes API calls have parameters, like a search string entered by the user. If the query parameters change, you should cancel any in-progress API calls. | ||
```tsx | ||
function searchUsers(searchTerm: string): CancellablePromise<User[]> { | ||
// call the API | ||
// call the API | ||
} | ||
export function UserList() { | ||
const [searchTerm, setSearchTerm] = useState('') | ||
const [users, setUsers] = useState<User[]>([]) | ||
const [searchTerm, setSearchTerm] = useState(''); | ||
const [users, setUsers] = useState<User[]>([]); | ||
// In a real app you should debounce the searchTerm | ||
useEffect(() => { | ||
const cancellablePromise = searchUsers(searchTerm) | ||
.then(setUsers) | ||
.catch(console.error) | ||
// In a real app you should debounce the searchTerm | ||
useEffect(() => { | ||
const cancellablePromise = searchUsers(searchTerm) | ||
.then(setUsers) | ||
.catch(console.error); | ||
// The old API call gets canceled whenever searchTerm changes. This prevents | ||
// setUsers from being called with incorrect results if the API calls complete | ||
// out of order. | ||
return cancellablePromise.cancel | ||
}, [searchTerm]) | ||
// The old API call gets canceled whenever searchTerm changes. This prevents | ||
// setUsers from being called with incorrect results if the API calls complete | ||
// out of order. | ||
return cancellablePromise.cancel; | ||
}, [searchTerm]); | ||
return ( | ||
<div> | ||
<SearchInput searchTerm={searchTerm} onChange={setSearchTerm} /> | ||
{users.map((u) => { | ||
/* ... */ | ||
})} | ||
</div> | ||
) | ||
return ( | ||
<div> | ||
<SearchInput searchTerm={searchTerm} onChange={setSearchTerm} /> | ||
{users.map((u) => { | ||
/* ... */ | ||
})} | ||
</div> | ||
); | ||
} | ||
@@ -272,15 +274,15 @@ ``` | ||
function bigQuery(userId: number): CancellablePromise<QueryResult> { | ||
return buildCancellablePromise(async (capture) => { | ||
const userPromise = api.user.get(userId) | ||
const rolePromise = api.user.listRoles(userId) | ||
return buildCancellablePromise(async (capture) => { | ||
const userPromise = api.user.get(userId); | ||
const rolePromise = api.user.listRoles(userId); | ||
const [user, roles] = await capture( | ||
CancellablePromise.all([userPromise, rolePromise]) | ||
) | ||
const [user, roles] = await capture( | ||
CancellablePromise.all([userPromise, rolePromise]) | ||
); | ||
// User must be loaded before this query can run | ||
const customer = await capture(api.customer.get(user.customerId)) | ||
// User must be loaded before this query can run | ||
const customer = await capture(api.customer.get(user.customerId)); | ||
return { user, roles, customer } | ||
}) | ||
return { user, roles, customer }; | ||
}); | ||
} | ||
@@ -303,11 +305,11 @@ ``` | ||
try { | ||
await capture(cancellablePromise) | ||
await capture(cancellablePromise); | ||
} catch (e) { | ||
if (e instanceof Cancellation) { | ||
// do nothing — the component probably just unmounted. | ||
// or you could do something here it's up to you 😆 | ||
return | ||
} | ||
if (e instanceof Cancellation) { | ||
// do nothing — the component probably just unmounted. | ||
// or you could do something here it's up to you 😆 | ||
return; | ||
} | ||
// log the error or display it to the user | ||
// log the error or display it to the user | ||
} | ||
@@ -322,8 +324,8 @@ ``` | ||
```ts | ||
const cancellablePromise = pseudoCancellable(normalPromise) | ||
const cancellablePromise = pseudoCancellable(normalPromise); | ||
// Later... | ||
cancellablePromise.cancel() | ||
cancellablePromise.cancel(); | ||
await cancellablePromise // throws Cancellation object if promise did not already resolve | ||
await cancellablePromise; // throws Cancellation object if promise did not already resolve | ||
``` | ||
@@ -334,3 +336,3 @@ | ||
```ts | ||
await CancellablePromise.delay(1000) // wait 1 second | ||
await CancellablePromise.delay(1000); // wait 1 second | ||
``` | ||
@@ -337,0 +339,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
44551
861
341
24