Comparing version 1.1.0 to 2.0.0
134
dist/caf.js
/*! caf.js | ||
v1.1.0 (c) 2018 Kyle Simpson | ||
v2.0.0 (c) 2018 Kyle Simpson | ||
MIT License: http://getify.mit-license.org | ||
*/ | ||
// polyfill for AbortController, adapted from: https://github.com/mo/abortcontroller-polyfill | ||
/* istanbul ignore next */ | ||
(function UMD(context,definition){ | ||
if (typeof define === "function" && define.amd) { define(definition); } | ||
else { definition(context); } | ||
})(typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : this,function DEF(context){ | ||
"use strict"; | ||
if (context.AbortController) { | ||
return; | ||
} | ||
class Emitter { | ||
constructor() { | ||
const delegate = typeof document != "undefined" ? | ||
document.createDocumentFragment() : | ||
{}; | ||
const methods = ["addEventListener", "dispatchEvent", "removeEventListener"]; | ||
methods.forEach(method => | ||
this[method] = (...args) => delegate[method](...args) | ||
); | ||
} | ||
} | ||
class AbortSignal extends Emitter { | ||
constructor() { | ||
super(); | ||
this.aborted = false; | ||
} | ||
toString() { | ||
return "[object AbortSignal]"; | ||
} | ||
} | ||
class AbortController { | ||
constructor() { | ||
this.signal = new AbortSignal(); | ||
} | ||
abort() { | ||
this.signal.aborted = true; | ||
try { | ||
this.signal.dispatchEvent(new Event("abort")); | ||
} catch (e) { | ||
if (typeof document != "undefined") { | ||
// For Internet Explorer 11: | ||
const event = document.createEvent("Event"); | ||
event.initEvent("abort", false, true); | ||
this.signal.dispatchEvent(event); | ||
} | ||
} | ||
} | ||
toString() { | ||
return "[object AbortController]"; | ||
} | ||
} | ||
if (typeof Symbol !== "undefined" && Symbol.toStringTag) { | ||
// These are necessary to make sure that we get correct output for: | ||
// Object.prototype.toString.call(new AbortController()) | ||
AbortController.prototype[Symbol.toStringTag] = "AbortController"; | ||
AbortSignal.prototype[Symbol.toStringTag] = "AbortSignal"; | ||
} | ||
context.AbortController = AbortController; | ||
context.AbortSignal = AbortSignal; | ||
}); | ||
(function UMD(name,context,definition){ | ||
/* istanbul ignore next */if (typeof define === "function" && define.amd) { define(definition); } | ||
/* istanbul ignore next */else if (typeof module !== "undefined" && module.exports) { module.exports = definition(); } | ||
/* istanbul ignore next */else if (typeof module !== "undefined" && module.exports) { module.exports = definition(name,context); } | ||
/* istanbul ignore next */else { context[name] = definition(name,context); } | ||
@@ -13,4 +81,21 @@ })("CAF",this,function DEF(name,context){ | ||
cancelToken.prototype.cancel = cancel; | ||
cancelToken.prototype.listen = listen; | ||
class cancelSignal extends AbortSignal { | ||
constructor() { | ||
super(); | ||
this.pr = new Promise((_,rej)=>this.rej = rej); | ||
this.pr.catch(_=>1); // silence unhandled rejection warnings | ||
} | ||
} | ||
class cancelToken extends AbortController { | ||
constructor() { | ||
super(); | ||
this.signal = new cancelSignal(); | ||
} | ||
abort(reason) { | ||
super.abort(); | ||
this.signal.rej(reason); | ||
} | ||
} | ||
CAF.cancelToken = cancelToken; | ||
@@ -26,13 +111,11 @@ | ||
return function instance(cancelToken,...args){ | ||
var trigger; | ||
var canceled = new Promise(function c(_,rej){ | ||
trigger = rej; | ||
}); | ||
var { it, pr } = _runner.call(this,generatorFn,cancelToken,...args); | ||
cancelToken.listen(function onCancel(reason){ | ||
try { var ret = it.return(); } catch (err) {} | ||
trigger(ret.value !== undefined ? ret.value : reason); | ||
it = pr = trigger = null; | ||
var cancel = cancelToken.pr.catch(function onCancel(reason){ | ||
try { | ||
var ret = it.return(); | ||
throw ret.value !== undefined ? ret.value : reason; | ||
} | ||
finally { it = pr = cancel = null; } | ||
}); | ||
var race = Promise.race([ pr, canceled ]); | ||
var race = Promise.race([ pr, cancel ]); | ||
race.catch(_=>1); // silence unhandled rejection warnings | ||
@@ -43,27 +126,2 @@ return race; | ||
function cancelToken() { | ||
this.canceled = false; | ||
this.cancelationReason = undefined; | ||
this.listeners = []; | ||
} | ||
function cancel(reason) { | ||
this.cancelationReason = reason; | ||
this.canceled = true; | ||
// note: running in LIFO instead of FIFO order | ||
// to ensure that cascaded cancelations run in | ||
// expected order | ||
while (this.listeners.length > 0) { | ||
let cb = this.listeners.pop(); | ||
try { cb(reason); } catch (err) {} | ||
} | ||
} | ||
function listen(cb) { | ||
if (this.canceled) { | ||
try { cb(this.cancelationReason); } catch (err) {} | ||
} | ||
else { | ||
this.listeners.push(cb); | ||
} | ||
} | ||
// thanks to Benjamin Gruenbaum (@benjamingr on GitHub) for | ||
@@ -70,0 +128,0 @@ // big improvements here! |
{ | ||
"name": "async-caf", | ||
"version": "1.1.0", | ||
"version": "2.0.0", | ||
"description": "Wrapper for generators as cancelable async functions", | ||
@@ -5,0 +5,0 @@ "main": "./dist/caf.js", |
154
README.md
# Cancelable Async Functions (CAF) | ||
[![Build Status](https://travis-ci.org/getify/caf.svg?branch=master)](https://travis-ci.org/getify/caf) | ||
[![Build Status](https://travis-ci.org/getify/CAF.svg?branch=master)](https://travis-ci.org/getify/CAF) | ||
[![npm Module](https://badge.fury.io/js/async-caf.svg)](https://www.npmjs.org/package/async-caf) | ||
@@ -24,3 +24,3 @@ [![Dependencies](https://david-dm.org/getify/caf.svg)](https://david-dm.org/getify/caf) | ||
// function that when called, returns a promise. | ||
var main = CAF( function *main(cancelToken,url){ | ||
var main = CAF( function *main(signal,url){ | ||
var resp = yield ajax( url ); | ||
@@ -35,9 +35,9 @@ | ||
// returned promise | ||
main( token, "http://some.tld/other" ) | ||
main( token.signal, "http://some.tld/other" ) | ||
.then( onResponse, onCancelOrError ); | ||
// only wait 3 seconds for the request! | ||
// only wait 5 seconds for the request! | ||
setTimeout( function(){ | ||
token.cancel( "Request took too long!" ); | ||
}, 3000 ); | ||
token.abort( "Request took too long!" ); | ||
}, 5000 ); | ||
``` | ||
@@ -47,3 +47,3 @@ | ||
Moreover, the generator itself is provided the cancelation token (`cancelToken` parameter above), so you can call another `function*` generator with **CAF**, and pass along the shared cancelation token. In this way, a single cancelation signal cascades across however many `function*` generators are currently in the execution chain: | ||
Moreover, the generator itself is provided the cancelation token's signal (`signal` parameter above), so you can call another `function*` generator with **CAF**, and pass along the shared cancelation token signal. In this way, a single cancelation signal cascades across however many `function*` generators are currently in the execution chain: | ||
@@ -53,27 +53,27 @@ ```js | ||
var one = CAF( function *one(cancelToken,v){ | ||
return yield two(cancelToken,v); | ||
var one = CAF( function *one(signal,v){ | ||
return yield two(signal,v); | ||
} ); | ||
var two = CAF( function *two(cancelToken,v){ | ||
return yield three(cancelToken,v); | ||
var two = CAF( function *two(signal,v){ | ||
return yield three(signal,v); | ||
} ); | ||
var three = CAF( function* three(cancelToken,v){ | ||
var three = CAF( function* three(signal,v){ | ||
return yield ajax( `http://some.tld/?v=${v}` ); | ||
} ); | ||
one( token, 42 ); | ||
one( token.signal, 42 ); | ||
// cancel request if not completed in 5 seconds | ||
setTimeout(function(){ | ||
token.cancel(); | ||
// only wait 5 seconds for the request! | ||
setTimeout( function(){ | ||
token.abort( "Request took too long!" ); | ||
}, 5000 ); | ||
``` | ||
In this snippet, `one(..)` calls and waits on `two(..)`, `two(..)` calls and waits on `three(..)`, and `three(..)` calls and waits on `ajax(..)`. Because the same cancelation token is used for the 3 generators, if `token.cancel()` is executed while they're all still paused, they will all immediately abort. | ||
In this snippet, `one(..)` calls and waits on `two(..)`, `two(..)` calls and waits on `three(..)`, and `three(..)` calls and waits on `ajax(..)`. Because the same cancelation token is used for the 3 generators, if `token.abort()` is executed while they're all still paused, they will all immediately abort. | ||
**Note:** In this example, the cancelation token has no effect on the `ajax(..)` call, since that utility ostensibly doesn't provide cancelation capability. The Ajax request itself would still run to its completion (or error or whatever), but we've canceled the `one(..)`, `two(..)`, and `three(..)` functions that were waiting to process its response. | ||
**Note:** In this example, the cancelation token has no effect on the actual `ajax(..)` call itself, since that utility ostensibly doesn't provide cancelation capability; the Ajax request itself would still run to its completion (or error or whatever). We've only canceled the `one(..)`, `two(..)`, and `three(..)` functions that were waiting to process its response. See [`AbortController(..)`](#abortcontroller) and [Manual Cancelation Signal Handling](#manual-cancelation-signal-handling) below for addressing this concern. | ||
## Overview | ||
## Background/Motivation | ||
@@ -92,2 +92,4 @@ An `async function` and a `function*` generator (driven with a [generator-runner](https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch4.md#promise-aware-generator-runner)) look, generally speaking, very similar. For that reason, most people just prefer `async function` since it's a little nicer syntax and doesn't require a library to provide the runner. | ||
## Overview | ||
These two functions are essentially equivalent; `one(..)` is an actual `async function`, whereas `two(..)` will behave like an async function in that it also returns a promise: | ||
@@ -115,7 +117,7 @@ | ||
two( token, 21 ) | ||
two( token.signal, 21 ) | ||
.then( console.log, console.error ); // 42 | ||
``` | ||
If `token.cancel(..)` is executed while `two(..)` is still running, its promise will be rejected. If you pass a cancelation reason (any value, but typically a string) to `token.cancel(..)`, that will be passed as the promise rejection: | ||
If `token.abort(..)` is executed while `two(..)` is still running, the signal's promise will be rejected. If you pass a cancelation reason (any value, but typically a string) to `token.abort(..)`, that will be the promise rejection reason: | ||
@@ -126,7 +128,109 @@ ```js | ||
setTimeout( function(){ | ||
token.cancel( "Took too long!" ); | ||
}, 10 ); | ||
token.abort( "Took too long!" ); | ||
``` | ||
### `finally { .. }` | ||
Canceling a **CAF**-wrapped `function*` generator that is paused does cause it to abort right away, but if there's a pending `finally {..}` clause, that will still have a chance to run. | ||
Moreover, a `return` of a non-`undefined` value in that pending `finally {..}` clause will override the completion result of the function: | ||
```js | ||
var token = new CAF.cancelToken(); | ||
var main = CAF( function *main(signal,url){ | ||
try { | ||
return yield ajax( url ); | ||
} | ||
finally { | ||
return 42; | ||
} | ||
} ); | ||
main( token.signal, "http://some.tld/other" ) | ||
.catch( console.log ); // 42 <-- not "Aborting!" | ||
token.abort( "Aborting!" ); | ||
``` | ||
Whatever value is passed to `abort(..)`, if any, is normally the completion value (promise rejection reason) of the function. But in this case, `42` overrides the `"Aborting!"` value. | ||
### `AbortController(..)` | ||
`CAF.cancelToken(..)` extends [`AbortController`, the DOM standard](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) for canceling/aborting operations like `fetch(..)` calls. As such, a cancelation token's signal can be passed directly to a DOM API like `fetch(..)` and it will respond to it accordingly: | ||
```js | ||
var token = new CAF.cancelToken(); | ||
var main = CAF(function *main(signal,url) { | ||
var resp = await fetch( url, { signal } ); | ||
console.log( resp ); | ||
return resp; | ||
}); | ||
main( token.signal, "http://some.tld/other" ) | ||
.catch( console.log ); // "Aborting!" | ||
token.abort( "Aborting!" ); | ||
``` | ||
**Note:** If the standard `AbortController` is not defined in the environment, it's [polyfilled](https://github.com/mo/abortcontroller-polyfill). | ||
### Manual Cancelation Signal Handling | ||
Even if you aren't calling a cancelation signal-aware utility (like `fetch(..)`), you can still manually listen to the cancelation signal: | ||
```js | ||
var token = new CAF.cancelToken(); | ||
var main = CAF( function *main(signal,url){ | ||
// listen to the signal's promise rejection directly | ||
signal.pr.catch( reason => { | ||
// reason == "Aborting!" | ||
} ); | ||
var resp = yield ajax( url ); | ||
console.log( resp ); | ||
return resp; | ||
} ); | ||
main( token.signal, "http://some.tld/other" ) | ||
.catch( console.log ); // "Aborting!" | ||
token.abort( "Aborting!" ); | ||
``` | ||
**Note:** The `catch(..)` handler inside of `main(..)` will still run, even though `main(..)` will be aborted at its waiting `yield` statement. If there was a way to manually cancel the `ajax(..)` call, that code could run here. | ||
And even if you aren't running in a **CAF**-wrapped function, you can still respond to the cancelation signal manually to interrupt flow control: | ||
```js | ||
var token = new CAF.cancelToken(); | ||
// normal async function | ||
async function main(signal,url) { | ||
try { | ||
var resp = await Promise.race( [ | ||
ajax( url ), | ||
signal.pr | ||
] ); | ||
console.log( resp ); | ||
return resp; | ||
} | ||
catch (err) { | ||
// err == "Aborting!" | ||
} | ||
} | ||
main( token.signal, "http://some.tld/other" ) | ||
.catch( console.log ); // "Aborting!" | ||
token.abort( "Aborting!" ); | ||
``` | ||
**Note:** As discussed earlier, the `ajax(..)` call itself is not cancelation aware, and is thus not being aborted here. But we *are* aborting our waiting on the `ajax(..)` call. When `signal.pr` wins the `Promise.race(..)` race and creates an exception from its promise rejection, flow control jumps to the `catch (err) { .. }` clause. | ||
## npm Package | ||
@@ -148,3 +252,3 @@ | ||
[![Build Status](https://travis-ci.org/getify/caf.svg?branch=master)](https://travis-ci.org/getify/caf) | ||
[![Build Status](https://travis-ci.org/getify/CAF.svg?branch=master)](https://travis-ci.org/getify/CAF) | ||
[![npm Module](https://badge.fury.io/js/async-caf.svg)](https://www.npmjs.org/package/async-caf) | ||
@@ -151,0 +255,0 @@ |
@@ -13,12 +13,14 @@ #!/usr/bin/env node | ||
SRC_DIR = path.join(ROOT_DIR,"src"), | ||
LIB_DIR = path.join(ROOT_DIR,"lib"), | ||
DIST_DIR = path.join(ROOT_DIR,"dist"), | ||
LIB_SRC = path.join(SRC_DIR,"caf.src.js"), | ||
LIB_DIST = path.join(DIST_DIR,"caf.js"), | ||
POLYFILL_SRC = path.join(LIB_DIR,"abortcontroller-polyfill-modified.js"), | ||
CORE_SRC = path.join(SRC_DIR,"caf.src.js"), | ||
CORE_DIST = path.join(DIST_DIR,"caf.js"), | ||
result | ||
result = "" | ||
; | ||
console.log("*** Building Core ***"); | ||
console.log(`Building: ${LIB_DIST}`); | ||
console.log(`Building: ${CORE_DIST}`); | ||
@@ -32,4 +34,6 @@ try { | ||
result += fs.readFileSync(POLYFILL_SRC,{ encoding: "utf8" }); | ||
result += "\n" + fs.readFileSync(CORE_SRC,{ encoding: "utf8" }); | ||
// NOTE: since uglify doesn't yet support ES6, no minifying happening :( | ||
result = fs.readFileSync(LIB_SRC,{ encoding: "utf8" }); | ||
@@ -68,3 +72,3 @@ // result = ugly.minify(path.join(SRC_DIR,"caf.src.js"),{ | ||
// write dist | ||
fs.writeFileSync( LIB_DIST, result /* result.code + "\n" */, { encoding: "utf8" } ); | ||
fs.writeFileSync( CORE_DIST, result /* result.code + "\n" */, { encoding: "utf8" } ); | ||
@@ -71,0 +75,0 @@ console.log("Complete."); |
@@ -14,2 +14,3 @@ #!/usr/bin/env node | ||
else { | ||
require(path.join("..","lib","abortcontroller-polyfill-modified.js")); | ||
global.CAF = require(path.join("..","src","caf.src.js")); | ||
@@ -16,0 +17,0 @@ } |
(function UMD(name,context,definition){ | ||
/* istanbul ignore next */if (typeof define === "function" && define.amd) { define(definition); } | ||
/* istanbul ignore next */else if (typeof module !== "undefined" && module.exports) { module.exports = definition(); } | ||
/* istanbul ignore next */else if (typeof module !== "undefined" && module.exports) { module.exports = definition(name,context); } | ||
/* istanbul ignore next */else { context[name] = definition(name,context); } | ||
@@ -8,4 +8,21 @@ })("CAF",this,function DEF(name,context){ | ||
cancelToken.prototype.cancel = cancel; | ||
cancelToken.prototype.listen = listen; | ||
class cancelSignal extends AbortSignal { | ||
constructor() { | ||
super(); | ||
this.pr = new Promise((_,rej)=>this.rej = rej); | ||
this.pr.catch(_=>1); // silence unhandled rejection warnings | ||
} | ||
} | ||
class cancelToken extends AbortController { | ||
constructor() { | ||
super(); | ||
this.signal = new cancelSignal(); | ||
} | ||
abort(reason) { | ||
super.abort(); | ||
this.signal.rej(reason); | ||
} | ||
} | ||
CAF.cancelToken = cancelToken; | ||
@@ -21,13 +38,11 @@ | ||
return function instance(cancelToken,...args){ | ||
var trigger; | ||
var canceled = new Promise(function c(_,rej){ | ||
trigger = rej; | ||
}); | ||
var { it, pr } = _runner.call(this,generatorFn,cancelToken,...args); | ||
cancelToken.listen(function onCancel(reason){ | ||
try { var ret = it.return(); } catch (err) {} | ||
trigger(ret.value !== undefined ? ret.value : reason); | ||
it = pr = trigger = null; | ||
var cancel = cancelToken.pr.catch(function onCancel(reason){ | ||
try { | ||
var ret = it.return(); | ||
throw ret.value !== undefined ? ret.value : reason; | ||
} | ||
finally { it = pr = cancel = null; } | ||
}); | ||
var race = Promise.race([ pr, canceled ]); | ||
var race = Promise.race([ pr, cancel ]); | ||
race.catch(_=>1); // silence unhandled rejection warnings | ||
@@ -38,27 +53,2 @@ return race; | ||
function cancelToken() { | ||
this.canceled = false; | ||
this.cancelationReason = undefined; | ||
this.listeners = []; | ||
} | ||
function cancel(reason) { | ||
this.cancelationReason = reason; | ||
this.canceled = true; | ||
// note: running in LIFO instead of FIFO order | ||
// to ensure that cascaded cancelations run in | ||
// expected order | ||
while (this.listeners.length > 0) { | ||
let cb = this.listeners.pop(); | ||
try { cb(reason); } catch (err) {} | ||
} | ||
} | ||
function listen(cb) { | ||
if (this.canceled) { | ||
try { cb(this.cancelationReason); } catch (err) {} | ||
} | ||
else { | ||
this.listeners.push(cb); | ||
} | ||
} | ||
// thanks to Benjamin Gruenbaum (@benjamingr on GitHub) for | ||
@@ -65,0 +55,0 @@ // big improvements here! |
"use strict"; | ||
QUnit.test( "API", function test(assert){ | ||
assert.expect( 5 ); | ||
assert.expect( 6 ); | ||
@@ -9,7 +9,8 @@ assert.ok( _isFunction( CAF ), "CAF()" ); | ||
assert.ok( _isFunction( CAF.cancelToken ), "CAF.cancelToken()" ); | ||
assert.ok( _isFunction( (new CAF.cancelToken()).listen ), "CAF.cancelToken#listen()" ); | ||
assert.ok( _isFunction( (new CAF.cancelToken()).cancel ), "CAF.cancelToken#cancel()" ); | ||
assert.ok( _isObject( (new CAF.cancelToken()).signal ), "CAF.cancelToken#signal" ); | ||
assert.ok( _isObject( (new CAF.cancelToken()).signal.pr ), "CAF.cancelToken#signal.pr" ); | ||
assert.ok( _isFunction( (new CAF.cancelToken()).abort ), "CAF.cancelToken#abort()" ); | ||
} ); | ||
QUnit.test( "cancelToken", function test(assert){ | ||
QUnit.test( "cancelToken.abort()", async function test(assert){ | ||
function checkParameter(reason) { | ||
@@ -24,15 +25,13 @@ assert.step(reason); | ||
"quit", | ||
"---", | ||
"quit", | ||
]; | ||
token.listen(checkParameter); | ||
token.listen(checkParameter); | ||
token.cancel("quit"); | ||
assert.step("---"); | ||
token.listen(checkParameter); | ||
token.signal.pr.catch(checkParameter); | ||
token.abort("quit"); | ||
token.signal.pr.catch(checkParameter); | ||
await token.signal.pr.catch(_=>1); | ||
// rActual; | ||
assert.expect( 5 ); // note: 1 assertion + 4 `step(..)` calls | ||
assert.expect( 3 ); // note: 1 assertion + 2 `step(..)` calls | ||
assert.verifySteps( rExpected, "cancelation reason passed" ); | ||
@@ -44,3 +43,3 @@ } ); | ||
assert.step(this.x); | ||
assert.step(String(cancelToken === token)); | ||
assert.step(String(cancelToken === token.signal)); | ||
assert.step(a); | ||
@@ -68,3 +67,3 @@ assert.step(b); | ||
// rActual; | ||
var pActual = asyncFn.call(obj,token,"3","12"); | ||
var pActual = asyncFn.call(obj,token.signal,"3","12"); | ||
var qActual = await pActual; | ||
@@ -100,3 +99,3 @@ pActual = pActual.toString(); | ||
setTimeout(function(){ | ||
token.cancel("Quit!"); | ||
token.abort("Quit!"); | ||
},50); | ||
@@ -106,3 +105,3 @@ | ||
try { | ||
await main(token,20); | ||
await main(token.signal,20); | ||
} | ||
@@ -143,3 +142,3 @@ catch (err) { | ||
setTimeout(function(){ | ||
token.cancel(); | ||
token.abort(); | ||
},50); | ||
@@ -149,3 +148,3 @@ | ||
try { | ||
await main(token,20); | ||
await main(token.signal,20); | ||
} | ||
@@ -197,4 +196,4 @@ catch (err) { | ||
"secondary: 3", | ||
"main: done", | ||
"secondary: done", | ||
"main: done", | ||
]; | ||
@@ -207,3 +206,3 @@ var pExpected = "Quit!"; | ||
setTimeout(function(){ | ||
token.cancel("Quit!"); | ||
token.abort("Quit!"); | ||
},50); | ||
@@ -213,3 +212,3 @@ | ||
try { | ||
await main(token,20); | ||
await main(token.signal,20); | ||
} | ||
@@ -216,0 +215,0 @@ catch (err) { |
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
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
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
34786
13
660
325
8