real-cancellable-promise
Advanced tools
Comparing version 1.0.0 to 1.1.0-alpha.0
@@ -12,2 +12,11 @@ # Contributing to real-cancellable-promise | ||
## Publishing | ||
1. Make sure that the CI workflow succeeded. | ||
2. Increment version in `package.json`. | ||
3. (If production release) Add a git tag in the format `v1.0.0`. | ||
4. Commit and push. Remember to push tags as well with `git push --tags`. | ||
5. `yarn npm publish` or `yarn npm publish --tag next`. The `prepublish` script will automatically do a clean and build. | ||
6. (If production release) Create a new release in GitHub. | ||
## TypeDoc | ||
@@ -14,0 +23,0 @@ |
@@ -1,15 +0,261 @@ | ||
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
__exportStar(require("./Cancellation"), exports); | ||
__exportStar(require("./CancellablePromise"), exports); | ||
__exportStar(require("./utils"), exports); | ||
/** | ||
* If canceled, a [[`CancellablePromise`]] should throw an `Cancellation` object. | ||
*/ | ||
class Cancellation extends Error { | ||
constructor(message = 'Promise canceled.') { | ||
super(message); | ||
} | ||
} | ||
/** @internal */ | ||
const noop = () => { }; | ||
/** | ||
* Determines if an arbitrary value is a thenable with a cancel method. | ||
*/ | ||
function isPromiseWithCancel(value) { | ||
return (typeof value === 'object' && | ||
typeof value.then === 'function' && | ||
typeof value.cancel === 'function'); | ||
} | ||
/** | ||
* A promise with a `cancel` method. | ||
* | ||
* If canceled, the `CancellablePromise` will reject with a [[`Cancellation`]] | ||
* object. | ||
* | ||
* @typeParam T what the `CancellablePromise` resolves to | ||
*/ | ||
class CancellablePromise { | ||
/** | ||
* @param promise a normal promise or thenable | ||
* @param cancel a function that cancels `promise`. **Calling `cancel` after | ||
* `promise` has resolved must be a no-op.** | ||
*/ | ||
constructor(promise, cancel) { | ||
this.promise = Promise.resolve(promise); | ||
this.cancel = cancel; | ||
} | ||
/** | ||
* Analogous to `Promise.then`. | ||
* | ||
* `onFulfilled` on `onRejected` can return a value, a normal promise, or a | ||
* `CancellablePromise`. So you can make a chain a `CancellablePromise`s | ||
* like this: | ||
* | ||
* ``` | ||
* const overallPromise = cancellableAsyncFunction1() | ||
* .then(cancellableAsyncFunction2) | ||
* .then(cancellableAsyncFunction3) | ||
* .then(cancellableAsyncFunction4) | ||
* ``` | ||
* | ||
* Then if you call `overallPromise.cancel`, `cancel` is called on all | ||
* `CancellablePromise`s in the chain! In practice, this means that | ||
* whichever async operation is in progress will be canceled. | ||
* | ||
* @returns a new CancellablePromise | ||
*/ | ||
then(onFulfilled, onRejected) { | ||
let fulfill; | ||
let reject; | ||
let callbackPromiseWithCancel; | ||
if (onFulfilled) { | ||
fulfill = (value) => { | ||
const nextValue = onFulfilled(value); | ||
if (isPromiseWithCancel(nextValue)) | ||
callbackPromiseWithCancel = nextValue; | ||
return nextValue; | ||
}; | ||
} | ||
if (onRejected) { | ||
reject = (reason) => { | ||
const nextValue = onRejected(reason); | ||
if (isPromiseWithCancel(nextValue)) | ||
callbackPromiseWithCancel = nextValue; | ||
return nextValue; | ||
}; | ||
} | ||
const newPromise = this.promise.then(fulfill, reject); | ||
const newCancel = () => { | ||
this.cancel(); | ||
callbackPromiseWithCancel === null || callbackPromiseWithCancel === void 0 ? void 0 : callbackPromiseWithCancel.cancel(); | ||
}; | ||
return new CancellablePromise(newPromise, newCancel); | ||
} | ||
/** | ||
* Analogous to `Promise.catch`. | ||
*/ | ||
catch(onRejected) { | ||
return this.then(undefined, onRejected); | ||
} | ||
/** | ||
* Attaches a callback that is invoked when the Promise is settled | ||
* (fulfilled or rejected). The resolved value cannot be modified from the | ||
* callback. | ||
* @param onFinally The callback to execute when the Promise is settled | ||
* (fulfilled or rejected). | ||
* @returns A Promise for the completion of the callback. | ||
*/ | ||
finally(onFinally) { | ||
return new CancellablePromise(this.promise.finally(onFinally), this.cancel); | ||
} | ||
static resolve(value) { | ||
return new CancellablePromise(Promise.resolve(value), noop); | ||
} | ||
/** | ||
* Analogous to `Promise.reject`. | ||
* | ||
* Like `CancellablePromise.resolve`, canceling the returned | ||
* `CancellablePromise` is a no-op. | ||
* | ||
* @param reason this should probably be an `Error` object | ||
*/ | ||
static reject(reason) { | ||
return new CancellablePromise(Promise.reject(reason), noop); | ||
} | ||
/** | ||
* Analogous to `Promise.all`. | ||
* | ||
* @param values an array that may contain `CancellablePromise`s, promises, | ||
* thenables, and resolved values | ||
* @returns a [[`CancellablePromise`]], which, if canceled, will cancel each | ||
* of the promises passed in to `CancellablePromise.all`. | ||
*/ | ||
static all(values) { | ||
return new CancellablePromise(Promise.all(values), () => { | ||
for (const value of values) { | ||
if (isPromiseWithCancel(value)) | ||
value.cancel(); | ||
} | ||
}); | ||
} | ||
static allSettled(values) { | ||
const cancel = () => { | ||
for (const value of values) { | ||
if (isPromiseWithCancel(value)) { | ||
value.cancel(); | ||
} | ||
} | ||
}; | ||
return new CancellablePromise(Promise.allSettled(values), cancel); | ||
} | ||
/** | ||
* Creates a `CancellablePromise` that is resolved or rejected when any of | ||
* the provided `Promises` are resolved or rejected. | ||
* @param values An array of `Promises`. | ||
* @returns A new `CancellablePromise`. Canceling it cancels all of the input | ||
* promises. | ||
*/ | ||
static race(values) { | ||
const cancel = () => { | ||
for (const value of values) { | ||
if (isPromiseWithCancel(value)) { | ||
value.cancel(); | ||
} | ||
} | ||
}; | ||
return new CancellablePromise(Promise.race(values), cancel); | ||
} | ||
// Promise.any is an ES2021 feature. Not yet implemented. | ||
// /** | ||
// * The any function returns a `CancellablePromise` that is fulfilled by the | ||
// * first given promise to be fulfilled, or rejected with an `AggregateError` | ||
// * containing an array of rejection reasons if all of the given promises are | ||
// * rejected. It resolves all elements of the passed iterable to promises as | ||
// * it runs this algorithm. | ||
// * @param values An array or iterable of Promises. | ||
// * @returns A new `CancellablePromise`. | ||
// */ | ||
// any<T>(values: (T | PromiseLike<T>)[] | Iterable<T | PromiseLike<T>>): CancellablePromise<T> { | ||
// return new CancellablePromise(Promise.any(values), cancel)) | ||
// } | ||
/** | ||
* @returns a `CancellablePromise` that resolves after `ms` milliseconds. | ||
*/ | ||
static delay(ms) { | ||
let timer; | ||
let rejectFn = noop; | ||
const promise = new Promise((resolve, reject) => { | ||
timer = setTimeout(() => { | ||
resolve(); | ||
rejectFn = noop; | ||
}, ms); | ||
rejectFn = reject; | ||
}); | ||
return new CancellablePromise(promise, () => { | ||
if (timer) | ||
clearTimeout(timer); | ||
rejectFn(new Cancellation()); | ||
}); | ||
} | ||
} | ||
/** | ||
* Takes in a regular `Promise` and returns a `CancellablePromise`. If canceled, | ||
* the `CancellablePromise` will immediately reject with a `Cancellation`, but the asynchronous | ||
* operation will not truly be aborted. | ||
* | ||
* Analogous to | ||
* [make-cancellable-promise](https://www.npmjs.com/package/make-cancellable-promise). | ||
*/ | ||
function pseudoCancellable(promise) { | ||
let canceled = false; | ||
let rejectFn = noop; | ||
const newPromise = new Promise((resolve, reject) => { | ||
rejectFn = reject; | ||
// eslint-disable-next-line promise/catch-or-return -- no catch method on PromiseLike | ||
promise.then((result) => { | ||
if (!canceled) { | ||
resolve(result); | ||
rejectFn = noop; | ||
} | ||
return undefined; | ||
}, (e) => { | ||
if (!canceled) | ||
reject(e); | ||
}); | ||
}); | ||
function cancel() { | ||
canceled = true; | ||
rejectFn(new Cancellation()); | ||
} | ||
return new CancellablePromise(newPromise, cancel); | ||
} | ||
/** | ||
* Used to build a single [[`CancellablePromise`]] from a multi-step asynchronous | ||
* flow. | ||
* | ||
* When the overall promise is canceled, each captured promise is canceled. In practice, | ||
* this means the active asynchronous operation is canceled. | ||
* | ||
* ``` | ||
* function query(id: number): CancellablePromise<QueryResult> { | ||
* return buildCancellablePromise(async capture => { | ||
* const result1 = await capture(api.method1(id)) | ||
* | ||
* // do some stuff | ||
* | ||
* const result2 = await capture(api.method2(result1.id)) | ||
* | ||
* return { result1, result2 } | ||
* }) | ||
* } | ||
* ``` | ||
* | ||
* @param innerFunc an async function that takes in a `capture` function and returns | ||
* a regular `Promise` | ||
*/ | ||
function buildCancellablePromise(innerFunc) { | ||
const capturedPromises = []; | ||
const capture = (promise) => { | ||
capturedPromises.push(promise); | ||
return promise; | ||
}; | ||
function cancel() { | ||
capturedPromises.forEach((p) => p.cancel()); | ||
} | ||
return new CancellablePromise(innerFunc(capture), cancel); | ||
} | ||
export { CancellablePromise, Cancellation, buildCancellablePromise, isPromiseWithCancel, pseudoCancellable }; |
{ | ||
"name": "real-cancellable-promise", | ||
"version": "1.0.0", | ||
"version": "1.1.0-alpha.0", | ||
"description": "A simple cancellable promise implementation that cancels the underlying HTTP call.", | ||
@@ -18,7 +18,8 @@ "keywords": [ | ||
"license": "MIT", | ||
"main": "dist/index.js", | ||
"main": "dist/index.cjs", | ||
"module": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"scripts": { | ||
"clean": "rimraf dist", | ||
"build": "yarn clean && tsc", | ||
"build": "yarn clean && rollup -c", | ||
"test": "jest", | ||
@@ -28,4 +29,14 @@ "lint": "eslint", | ||
"prettier-all": "prettier '**/*.ts?(x)'", | ||
"prepublish": "yarn build" | ||
"prepublish": "yarn build && pinst --disable", | ||
"postpublish": "pinst --enable", | ||
"postinstall": "husky install", | ||
"lint-staged": "lint-staged --no-stash" | ||
}, | ||
"lint-staged": { | ||
"*.ts?(x)": [ | ||
"eslint --max-warnings 0 --fix", | ||
"prettier --write" | ||
], | ||
"*.{md,js,cjs,yml,json}": "prettier --write" | ||
}, | ||
"devDependencies": { | ||
@@ -35,2 +46,3 @@ "@babel/core": "^7.15.5", | ||
"@babel/preset-typescript": "^7.15.0", | ||
"@rollup/plugin-typescript": "^8.2.5", | ||
"@types/jest": "^27.0.1", | ||
@@ -46,5 +58,10 @@ "@typescript-eslint/eslint-plugin": "^4.31.1", | ||
"eslint-plugin-promise": "^5.1.0", | ||
"husky": "^7.0.2", | ||
"jest": "^27.2.0", | ||
"lint-staged": "^11.1.2", | ||
"pinst": "^2.1.6", | ||
"prettier": "^2.4.0", | ||
"rimraf": "^3.0.2", | ||
"rollup": "^2.57.0", | ||
"tslib": "^2.3.1", | ||
"typedoc": "^0.22.3", | ||
@@ -51,0 +68,0 @@ "typescript": "^4.4.3" |
# real-cancellable-promise | ||
A simple cancellable promise implementation for JavaScript and TypeScript. | ||
Unlike [p-cancelable](https://www.npmjs.com/package/p-cancelable) and | ||
[make-cancellable-promise](https://www.npmjs.com/package/make-cancellable-promise) | ||
which only prevent your promise's callbacks from executing, | ||
**`real-cancellable-promise` cancels the underlying asynchronous operation | ||
(usually an HTTP call).** That's why it's called **Real** Cancellable Promise. | ||
[Read the announcement post for a full explanation.](https://dev.to/srmagura/announcing-real-cancellable-promise-gkd) In particular, see the "Prior art" section for a comparison to existing cancellable promise libraries. | ||
- ⚛ Built with React in mind — no more "setState after unmount" warnings! | ||
- ⚡ Compatible with [fetch](#fetch), [axios](#axios), and | ||
[jQuery.ajax](#jQuery) | ||
- ⚛ Built with React in mind — no more "setState after unmount" errors! | ||
- 🐦 Lightweight — zero dependencies and less than 1 kB minified and gzipped | ||
@@ -37,2 +34,13 @@ - 🏭 Used in production by [Interface | ||
### Important | ||
The `CancellablePromise` constructor takes in a `promise` and a `cancel` function. | ||
Your `cancel` function **MUST** cause `promise` to reject with a `Cancellation` object. | ||
This will **NOT** work, your callbacks with still run: | ||
```ts | ||
new CancellablePromise(normalPromise, () => {}) | ||
``` | ||
# Usage with HTTP Libraries | ||
@@ -45,2 +53,33 @@ | ||
```ts | ||
export function cancellableFetch( | ||
input: RequestInfo, | ||
init: RequestInit = {} | ||
): CancellablePromise<Response> { | ||
const controller = new AbortController() | ||
const promise = fetch(input, { | ||
...init, | ||
signal: controller.signal, | ||
}).catch((e) => { | ||
if (e.name === 'AbortError') { | ||
throw new Cancellation() | ||
} | ||
// rethrow the original error | ||
throw e | ||
}) | ||
return new CancellablePromise<Response>(promise, () => controller.abort()) | ||
} | ||
// Use just like normal fetch: | ||
const cancellablePromise = cancellableFetch(url, { | ||
/* pass options here */ | ||
}) | ||
``` | ||
<details> | ||
<summary><code>fetch</code> with response handling</summary> | ||
```ts | ||
export function cancellableFetch<T>( | ||
@@ -79,9 +118,6 @@ input: RequestInfo, | ||
} | ||
// Use just like normal fetch: | ||
const cancellablePromise = cancellableFetch(url, { | ||
/* pass options here */ | ||
}) | ||
``` | ||
</details> | ||
## <a name="axios" href="https://axios-http.com/">axios</a> | ||
@@ -137,7 +173,7 @@ | ||
# [API Reference](https://srmagura.github.io/real-cancellable-promise) | ||
# [API Reference](https://srmagura.github.io/real-cancellable-promise/modules.html) | ||
`CancellablePromise` supports all the methods that the normal `Promise` object | ||
supports, except `Promise.any` (ES2021). See the [API | ||
Reference](https://srmagura.github.io/real-cancellable-promise) for details. | ||
Reference](https://srmagura.github.io/real-cancellable-promise/modules.html) for details. | ||
@@ -275,4 +311,3 @@ # Examples | ||
Sometimes you need to call an asynchronous function that doesn't support | ||
cancellation. In this case, you can use `pseudoCancellable` to prevent the | ||
promise from resolving after `cancel` has been called. | ||
cancellation. In this case, you can use `pseudoCancellable`: | ||
@@ -288,8 +323,2 @@ ```ts | ||
This will **NOT** work, your callbacks with still run: | ||
```ts | ||
const cancellablePromise = new CancellablePromise(normalPromise, () => {}) | ||
``` | ||
## `CancellablePromise.delay` | ||
@@ -296,0 +325,0 @@ |
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
Install scripts
Supply chain riskInstall scripts are run when the package is installed. The majority of malware in npm is hidden in install scripts.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
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
392
46757
24
13
855
1
1
1