百科.dev
全部条目AI 编程趋势榜开源项目技术资讯提交条目
登录
< 返回工具列表
C

CAF

> 编程语言
开源

可取消异步流 (CAF)

1.4K stars0 点赞0 次浏览
访问官网GitHub

工具介绍

可取消异步流 (CAF)

Cancelable Async Flows (CAF)

CAF (/ˈkahf/) is a wrapper for function* generators that treats them like async functions, but with support for external cancellation via tokens. In this way, you can express flows of synchronous-looking asynchronous logic that are still cancelable (Cancelable Async Flows).

Also included is CAG(..), for alternately wrapping function* generators to emulate ES2018 async-generators (async function *).

Environment Support

This library uses ES2018 features. If you need to support environments prior to ES2018, transpile it first (with Babel, etc).

At A Glance

CAF (Cancelable Async Flows) wraps a function* generator so it looks and behaves like an async function, but that can be externally canceled using a cancellation token:

…

Create a cancellation token (via new CAF.cancelToken()) to pass into your wrapped function* generator, and then if you cancel the token, the function* generator will abort itself immediately, even if it's presently waiting on a promise to resolve.

The generator receives the cancellation token's signal, so from inside it you can call another function* generator via CAF and pass along that shared signal. In this way, a single cancellation signal can cascade across and cancel all the CAF-wrapped functions in a chain of execution:

var token = new CAF.cancelToken();

var one = CAF( function *one(signal,v){
    return yield two( signal, v );
} );

var two = CAF( function *two(signal,v){
    return yield three( signal, v );
} );

var three = CAF( function *three(signal,v){
    return yield ajax( `http://some.tld/?v=${v}` );
} );

one( token.signal, 42 );

// only wait 5 seconds for the request!
setTimeout( function onElapsed(){
    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 cancellation token is used for the 3 generators, if token.abort() is executed while they're all still paused, they will all immediately abort.

Note: The cancellation token has no effect on the actual ajax(..) call itself here, since that utility ostensibly doesn't provide cancellation 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(..) and Manual Cancellation Signal Handling below for addressing this limitation.

CAG: Cancelable Async Flows Generators

ES2018 added "async generators", which is a pairing of async function and function* -- so you can use await and yield in the same function, await for unwrapping a promise, and yield for pushing a value out. An async-generator (async function * f(..) { .. }), like regular iterators, is designed to be sequentially iterated, but using the "async iteration" protocol.

For example, in ES2018:

async function *stuff(urls) {
    for (let url of urls) {
        let resp = await fetch(url);  // await a promise
        yield resp.json();  // yield a value (even a promise for a value)
    }
}

// async-iteration loop
for await (let v of stuff(assetURLs)) {
    console.log(v);
}

In the same way that CAF(..) emulates an async..await function with a function* generator, the CAG(..) utility emulates an async-generator with a normal function* generator. You can cancel an async-iteration early (even if it's currently waiting internally on a promise) with a cancellation token.

You can also synchronously force-close an async-iterator by calling the return(..) on the iterator. With native async-iterators, return(..) is not actually synchronous, but CAG(..) patches this to allow synchronous closing.

Instead of yielding a promise the way you do with CAF(..), you use a provided pwait(..) function with yield, like yield pwait(somePromise). This allows a yield ..value.. expression for pushing out a value through the iterator, as opposed to yield pwait(..value..) to locally wait for the promise to resolve. To emulate a yield await ..value.. expression (common in async-generators), you use two yields together: yield yield pwait(..value..).

For example:

// NOTE: this is CAG(..), not to be confused with CAF(..)
var stuff = CAG(function *stuff({ signal, pwait },urls){
    for (let url of urls) {
        let resp = yield pwait(fetch(url,{ signal }));  // await a promise
        yield resp.json();  // yield a value (even a promise for a value)
    }
});

var timeout = CAF.timeout(5000,"That's enough results!");
var it = stuff(timeout,assetURLs);

cancelBtn.addEventListener("click",() => it.return("Stop!"),false);

// async-iteration loop
for await (let v of it) {
    console.log(v);
}

In this snippet, the stuff(..) async-iteration can either be canceled if the 5-second timeout expires before iteration is complete, or the click of the cancel button can force-close the iterator early. The difference between them is that token cancellation would result in an exception bubbling out (to the consuming loop), whereas calling return(..) will simply cleanly close the iterator (and halt the loop) with no exception.

Background/Motivation

Generally speaking, an async function and a function* generator (driven with a generator-runner) look very similar. For that reason, most people just prefer the async function form since it's a little nicer syntax and doesn't require a library for the runner.

However, there are limitations to async functions that come from having the syntax and engine make implicit assumptions that otherwise would have been handled by a function* generator runner.

One unfortunate limitation is that an async function cannot be externally canceled once it starts running. If you want to be able to cancel it, you have to intrusively modify its definition to have it consult an external value source -- like a boolean or promise -- at each line that you care about being a potential cancellation point. This is ugly and error-prone.

function* generators by contrast can be aborted at any time, using the iterator's return(..) method and/or by just not resuming the generator iterator instance with next(). But the downside of using function* generators is either needing a runner utility or the repetitive boilerplate of handling the iterator manually.

CAF provides a useful compromise: a function* generator that can be called like a normal async function, but which supports a cancellation token.

The CAF(..) utility wraps a function* generator with a normal promise-returing function, just as if it was an async function. Other than minor syntactic aesthetics, the major observable difference is that a CAF-wrapped function must be provided a cancellation token's signal as its first argument, with any other arguments passed subsequent, as desired.

By contrast, the CAG(..) utility wraps a function* generator as an ES2018 async-generator (async function *) that respects the native async-iteration protocol. Instead of await, you use yield pwait(..) in these emulated async-generators.

Overview

In the following snippet, the two functions are essentially equivalent; one(..) is an actual async function, whereas two(..) is a wrapper around a generator, but will behave like an async function in that it also returns a promise:

async function one(v) {
    await delay( 100 );
    return v * 2;
}

var two = CAF( function *two(signal,v){
    yield delay( 100 );
    return v * 2;
} );

Both one(..) and two(..) can be called directly with argument(s), and both return a promise for their completion:

one( 21 )
.then( console.log, console.error );   // 42

var token = new CAF.cancelToken();

two( token.signal, 21 )
.then( console.log, console.error );   // 42

If token.abort(..) is executed while two(..) is still running, the signal's promise will be rejected. If you pass a cancellation reason (any value, but typically a string) to token.abort(..), that will be the promise rejection reason:

two( token, 21 )
.then( console.log, console.error );    // Took too long!

token.abort( "Took too long!" );

Delays & Timeouts

One of the most common use-cases for cancellation of an async task is because too much time passes and a timeout threshold is passed.

As shown earlier, you can implement that sort of logic with a cancelToken() instance and a manual call to the environment's setTimeout(..). However, there are some subtle but important downsides to doing this kind of thing manually. These downsides are harder to spot in the browser, but are more obvious in Node.js

Consider this code:

function delay(ms) {
    return new Promise( function c(res){
        setTimeout( res, ms );
    } );
}

var token = new CAF.cancelToken();

var main = CAF( function *main(signal,ms){
    yield delay( ms );
    console.log( "All done!" );
} );

main( token.signal, 100 );

// only wait 5 seconds for the request!
delay( 5000 ).then( function onElapsed(){
    token.abort( "Request took too long!" );
} );

The main(..) function delays for 100ms and then completes. But there's no logic that clears the timeout set from delay( 5000 ), so it will continue to hold pending until that amount of time expires.

Of course, the token.abort(..) call at that point is moot, and is thus silently ignored. But the problem is the timer still running, which keeps a Node.js process alive even if the rest of the program has completed. The symptoms of this would be running a Node.js program from the command line and observing it "hang" for a bit at the end instead of exiting right away. Try the above code to see this in action.

There's two complications that make avoiding this downside tricky:

  1. The delay(..) helper shown, which is a promisified version of setTimeout(..), is basically what you can produce by using Node.js's util.promisify(..) against setTimeout(..). However, that timer itself is not cancelable. You can't access the timer handle (return value from setTimeout(..)) to call clearTimeout(..) on it. So, you can't stop the timer early even if you wanted to.

  2. If instead you set up your own timer externally, you need to keep track of the timer's handle so you can call clearTimeout(..) if the async task completes successfully before the timeout expires. This is manual and error-prone, as it's far too easy to forget.

Instead of inventing solutions to these problems, CAF provides two utilities for managing cancelable delays and timeout cancellations: CAF.delay(..) and CAF.timeout(..).

CAF.delay(..)

What we need is a promisified setTimeout(..), like delay(..) we saw in the previous section, but that can still be canceled. CAF.delay(..) provides us such functionality:

var discardTimeout = new CAF.cancelToken();

// a promise that waits 5 seconds
CAF.delay( discardTimeout.signal, 5000 )
.then(
    function onElapsed(msg){
        // msg: "delayed: 5000"
    },
    function onInterrupted(reason){
        // reason: "delay (5000) interrupted"
    }
);

As you can see, CAF.delay(..) receives a cancellation token signal to cancel the timeout early when needed. If you need to cancel the timeout early, abort the cancellation token:

discardTimeout.abort();     // cancel the `CAF.delay()` timeout

The promise returned from CAF.delay(..) is fulfilled if the full time amount elapses, with a message such as `"delayed

Issues· 0 开放

查看全部 Issues在 GitHub 打开

暂无开放 Issues,或尚未同步最近议题。

> 标签

JavaScriptasynccancelablejavascriptlibrary

暂无评论,来聊聊你的看法吧

> 工具信息

发布日期2026年8月1日
最后更新2026年9月17日
分类编程语言
定价开源

> 相关工具

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言