Skip to content

Commit

Permalink
Fixed timeout cancellation;
Browse files Browse the repository at this point in the history
Added live example of using timeouts;
Updated README.hbs.md;
  • Loading branch information
DigitalBrainJS committed Sep 11, 2020
1 parent 38320fc commit 3238d79
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 58 deletions.
71 changes: 44 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,58 @@ like cancellation, timeouts and progress capturing.
In terms of the library **the cancellation means rejection of the deepest promise in
the chain with a special error subclass**.

It supports cancellation of the whole chain, not just a single promise.
The cancellation could be handled by the above standing chains, since it's just
throwing a special error and invoking `onCancel` listeners and/or notify subscribers by the signals
using `AbortController` (built-in implementation or native if it's available).
**It supports cancellation of the whole chain, not just a single promise**.

This lib can be used on the backend and frontend sides.
This lib can be used for both backend and frontend development, no any dependencies required.

## Why :question:

You may face with a challenge when you need to cancel some long-term asynchronous
operation before it will be completed with success or failure, just because the result
has lost its relevance to you.

## How it works

The deepest pending CPromise in the chain will be rejected will a `CanceledError`,
then that chain and each above standing chain will emit `cancel` event. This event will be handled by
callbacks attached by `onCancel(cb)` method and propagate with signal from `AbortController`.
These api can be used simultaneously. The `cancel([reason])` method is synchronous and can be called any time.
If cancellation failed (the chain has been already fulfilled) it will return `false`.

## Features / Advantages
- there are no any dependencies (except [native] Promise)
- browser support
- :fire: supports cancellation of the whole chain - rejects the deepest pending promise in the chain
- supports onCancel event handler to abort some internal work (clear timers, close requests etc.)
- supports built-in signal interface for API that supports it (like fetch method)
- proper handling of `CanceledError` errors manually thrown inside the chain
- :fire: progress capturing with result scaling to handle progress of the whole chain (including nested promise chains), useful for long-term operations
- ability to install the `weight` for each promise in the chain
- ability to attach meta info on each setting of the progress
- the `delay` method to return promise that will be resolved with the value after timeout
- static methods `all`, `race` support cancellation and will cancel all other pending promises after they resolved
- the `catch` method supports error class filtering

## Live Example

This is how an abortable fetch ([live example](https://jsfiddle.net/DigitalBrain/c6njyrt9/10/)) with a timeout might look like
````javascript
function fetchWithTimeout(url, timeout) {
return new CPromise((resolve, reject, {signal}) => {
fetch(url, {signal}).then(resolve, reject)
}).timeout(timeout)
}

const chain= fetchWithTimeout('http://localhost/', 5000);
// chain.cancel();
````

[Live browser example (jsfiddle.net)](https://jsfiddle.net/DigitalBrain/g0dv5L8c/5/)

[Live nodejs example (jsfiddle.net)](https://runkit.com/digitalbrainjs/runkit-npm-c-promise2)

<img src="http://g.recordit.co/E6e97qRPoY.gif" alt="Browser playground with fetch" width="50%" height="50%">

## Installation :hammer:

- Install for node.js using npm/yarn:
Expand Down Expand Up @@ -58,28 +97,6 @@ CPromise.delay(1000, 'It works!').then(str => console.log('Done', str));

- [production ESM version](https://unpkg.com/c-promise2@0.1.0/dist/c-promise.mjs)

## Features / Advantages
- there are no any dependencies (except [native] Promise)
- browser support
- :fire: supports cancellation of the whole chain - rejects the deepest pending promise in the chain
- supports onCancel event handler to abort some internal work (clear timers, close requests etc.)
- supports built-in signal interface for API that supports it (like fetch method)
- proper handling of `CanceledError` errors manually thrown inside the chain
- :fire: progress capturing with result scaling to handle progress of the whole chain (including nested promise chains), useful for long-term operations
- ability to install the `weight` for each promise in the chain
- ability to attach meta info on each setting of the progress
- the `delay` method to return promise that will be resolved with the value after timeout
- static methods `all`, `race` support cancellation and will cancel all other pending promises after they resolved
- the `catch` method supports error class filtering

## Live Example

[Live browser example](https://jsfiddle.net/DigitalBrain/g0dv5L8c/5/)

[Live nodejs example](https://runkit.com/digitalbrainjs/runkit-npm-c-promise2)

<img src="http://g.recordit.co/E6e97qRPoY.gif" alt="Browser playground with fetch" width="50%" height="50%">

## Playground
- Clone https://github.com/DigitalBrainJS/c-promise.git repo
- Run npm install to install dev-dependencies
Expand Down
71 changes: 44 additions & 27 deletions jsdoc2md/README.hbs.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,58 @@ like cancellation, timeouts and progress capturing.
In terms of the library **the cancellation means rejection of the deepest promise in
the chain with a special error subclass**.

It supports cancellation of the whole chain, not just a single promise.
The cancellation could be handled by the above standing chains, since it's just
throwing a special error and invoking `onCancel` listeners and/or notify subscribers by the signals
using `AbortController` (built-in implementation or native if it's available).
**It supports cancellation of the whole chain, not just a single promise**.

This lib can be used on the backend and frontend sides.
This lib can be used for both backend and frontend development, no any dependencies required.

## Why :question:

You may face with a challenge when you need to cancel some long-term asynchronous
operation before it will be completed with success or failure, just because the result
has lost its relevance to you.

## How it works

The deepest pending CPromise in the chain will be rejected will a `CanceledError`,
then that chain and each above standing chain will emit `cancel` event. This event will be handled by
callbacks attached by `onCancel(cb)` method and propagate with signal from `AbortController`.
These api can be used simultaneously. The `cancel([reason])` method is synchronous and can be called any time.
If cancellation failed (the chain has been already fulfilled) it will return `false`.

## Features / Advantages
- there are no any dependencies (except [native] Promise)
- browser support
- :fire: supports cancellation of the whole chain - rejects the deepest pending promise in the chain
- supports onCancel event handler to abort some internal work (clear timers, close requests etc.)
- supports built-in signal interface for API that supports it (like fetch method)
- proper handling of `CanceledError` errors manually thrown inside the chain
- :fire: progress capturing with result scaling to handle progress of the whole chain (including nested promise chains), useful for long-term operations
- ability to install the `weight` for each promise in the chain
- ability to attach meta info on each setting of the progress
- the `delay` method to return promise that will be resolved with the value after timeout
- static methods `all`, `race` support cancellation and will cancel all other pending promises after they resolved
- the `catch` method supports error class filtering

## Live Example

This is how an abortable fetch ([live example](https://jsfiddle.net/DigitalBrain/c6njyrt9/10/)) with a timeout might look like
````javascript
function fetchWithTimeout(url, timeout) {
return new CPromise((resolve, reject, {signal}) => {
fetch(url, {signal}).then(resolve, reject)
}).timeout(timeout)
}

const chain= fetchWithTimeout('http://localhost/', 5000);
// chain.cancel();
````

[Live browser example (jsfiddle.net)](https://jsfiddle.net/DigitalBrain/g0dv5L8c/5/)

[Live nodejs example (jsfiddle.net)](https://runkit.com/digitalbrainjs/runkit-npm-c-promise2)

<img src="http://g.recordit.co/E6e97qRPoY.gif" alt="Browser playground with fetch" width="50%" height="50%">

## Installation :hammer:

- Install for node.js using npm/yarn:
Expand Down Expand Up @@ -58,28 +97,6 @@ CPromise.delay(1000, 'It works!').then(str => console.log('Done', str));

- [production ESM version](https://unpkg.com/c-promise2@0.1.0/dist/c-promise.mjs)

## Features / Advantages
- there are no any dependencies (except [native] Promise)
- browser support
- :fire: supports cancellation of the whole chain - rejects the deepest pending promise in the chain
- supports onCancel event handler to abort some internal work (clear timers, close requests etc.)
- supports built-in signal interface for API that supports it (like fetch method)
- proper handling of `CanceledError` errors manually thrown inside the chain
- :fire: progress capturing with result scaling to handle progress of the whole chain (including nested promise chains), useful for long-term operations
- ability to install the `weight` for each promise in the chain
- ability to attach meta info on each setting of the progress
- the `delay` method to return promise that will be resolved with the value after timeout
- static methods `all`, `race` support cancellation and will cancel all other pending promises after they resolved
- the `catch` method supports error class filtering

## Live Example

[Live browser example](https://jsfiddle.net/DigitalBrain/g0dv5L8c/5/)

[Live nodejs example](https://runkit.com/digitalbrainjs/runkit-npm-c-promise2)

<img src="http://g.recordit.co/E6e97qRPoY.gif" alt="Browser playground with fetch" width="50%" height="50%">

## Playground
- Clone https://github.com/DigitalBrainJS/c-promise.git repo
- Run npm install to install dev-dependencies
Expand Down
63 changes: 61 additions & 2 deletions lib/c-promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ const __AbortController =

const isThenable = obj => obj && typeof obj.then === 'function';

function isGeneratorFunction(thing){
return typeof thing==='function' && thing.constructor && thing.constructor.name === 'GeneratorFunction';
}

function isGenerator(thing){
return thing && typeof thing==='object' && typeof thing.next==='function' && typeof thing.throw==='function';
}

function toCPromise(thing){
return thing && thing instanceof CPromise? thing : CPromise.resolve(thing);
}

/**
* @typedef PromiseScopeOptions {Object}
* @property {String} label - the label for the promise
Expand Down Expand Up @@ -459,7 +471,7 @@ class CPromiseScope extends TinyEventEmitter {
*/

cancel(reason) {
throw CanceledError.from(reason);
return this[_cancel](CanceledError.from(reason));
}

[_cancel](err) {
Expand Down Expand Up @@ -628,7 +640,7 @@ class CPromise extends Promise {
*/

cancel(reason) {
return this[_scope][_cancel](CanceledError.from(reason));
return this[_scope].cancel(reason);
}

/**
Expand Down Expand Up @@ -742,6 +754,53 @@ class CPromise extends Promise {
});
});
}

static from(generatorFn, args){
return new this((resolve, reject, scope)=>{
if(!isGeneratorFunction(generatorFn)){
throw TypeError('value should be');
}

const generator= generatorFn.apply(scope, args);

let ret, resolvedValue;

onFulfilled();

function onFulfilled(res) {
if(ret.done){
return resolve(resolvedValue);
}

try {
ret = generator.next(res);
} catch (e) {
return reject(e);
}

resolveThat(ret);
return null;
}

function onRejected(err) {
let ret;
try {
ret = generator.throw(err);
} catch (e) {
return reject(e);
}
resolveThat(ret);
}

function resolveThat(ret) {
const {value}= ret;
if(isThenable(value)){
return toCPromise(ret.value).then(onFulfilled, onRejected);
}
resolve(value);
}
});
}
}

const {prototype}= CPromise;
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
"build": "rollup -c",
"build:watch": "nodemon --watch lib/ --exec \\\"npm run build\\\"",
"dev": "cross-env NODE_ENV=development \"npm run test:watch\"",
"playground": "node playground/basic.js || true",
"playground:watch": "nodemon --watch ./playground --watch lib/ --exec \\\"npm run build && npm run playground\\\"",
"playground": "node playground/generator.js || true",
"playground:watch": "nodemon --watch ./playground --watch lib/ --exec \\\"npm run playground\\\"",
"docs": "jsdoc2md -t jsdoc2md/README.hbs.md lib/c-promise.js > README.md",
"docs:namepaths": "jsdoc2md ./lib/c-promise.js --namepaths"
},
Expand Down
47 changes: 47 additions & 0 deletions playground/fetch-timeout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fetch timeout</title>
<script src="../dist/c-promise.umd.js"></script>
</head>
<body>
<p>Make request to endpoint with 10s+ latency</p>
<p>Open your console to see the log</p>
<p>Note that the related request does abort when you undo the promise chain (see the network tab in developer tools)</p>
<button onclick="request(0)">Request url</button>
<button onclick="request(5000)">Request url with timeout 5s</button>
<button onclick="abort()">Abort</button>
<script>
let fetchChain;
const url = "https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=10s";
let timestamp= Date.now();
const log= (message, ...values)=> console.log(`[${Date.now()-timestamp}ms] ${message}`, ...values);
const updateTimeStamp= ()=> (timestamp= Date.now());

function fetchWithTimeout(url, timeout) {
return new CPromise((resolve, reject, {signal}) => {
fetch(url, {signal}).then(resolve, reject)
}).timeout(timeout)
}

function request(timeout) {
abort();
updateTimeStamp();
log('Fetch started');
fetchChain = fetchWithTimeout(url, timeout)
.then(response => response.json())
.then(data => {
log(`Done: `, data);
},
err => {
log('Error:', err);
});
}

function abort() {
fetchChain && fetchChain.cancel();
}
</script>
</body>
</html>

0 comments on commit 3238d79

Please sign in to comment.