Asynquence
Asynchronous flow control (promises, generators, observables, CSP, etc)
Install / Use
/learn @getify/AsynquenceREADME
asynquence
Promise-style async sequence flow control.
Explanation
asynquence ("async" + "sequence") is an abstraction on top of promises (promise chains) that lets you express flow control steps with callbacks, promises, or generators.
If you're interested in detailed discussion about asynquence, here's some reading to check out:
- "You Don't Know JS: Async & Performance", Appendix A and Appendix B.
- Two-part blog post series on David Walsh's site.
TL;DR: By Example
- Sequences & gates, at a glance
- Refactoring "callback hell"-style to asynquence
- Example/explanation of promise-style sequences
- More advanced example of "nested" composition of sequences
- Iterable sequences: sync loop and async loop and async batch iteration of list
- "State Machine" with generator coroutines
- "Ping Pong", CSP-flavored coroutine concurrency, via
runner(..)contrib plugin. Also another example showing message passing. - go-Style CSP emulating goroutines with generator wrappers (via
runner(..)contrib plugin and CSP emulation adapter). - Event-Reactive Sequences (example 1, example 2) (via
react(..)plugin) inspired by RxJS's Reactive Observables - API Usage Examples
Sequences
Say you want to perform two or more asynchronous tasks one after the other (like animation delays, XHR calls, file I/O, etc). You need to set up an ordered series of tasks and make sure the previous one finishes before the next one is processed. You need a sequence.
You create a sequence by calling ASQ(...). Each time you call ASQ(), you create a new, separate sequence.
To create a new step, simply call then(...) with a function. That function will be executed when that step is ready to be processed, and it will be passed as its first parameter the completion trigger. Subsequent parameters, if any, will be any messages passed on from the immediately previous step.
The completion trigger that your step function(s) receive can be called directly to indicate success, or you can add the fail flag (see examples below) to indicate failure of that step. In either case, you can pass one or more messages onto the next step (or the next failure handler) by simply adding them as parameters to the call.
Example:
ASQ(21)
.then(function(done,msg){
setTimeout(function(){
done(msg * 2);
},10);
})
.then(function(done,msg){
done("Meaning of life: " + msg);
})
.then(function(done,msg){
msg; // "Meaning of life: 42"
});
Note: then(..) can also receive other asynquence sequence instances directly, just as seq(..) can (see below). When you call then(Sq), the Sq sequence is tapped immediately, but the success/error message streams of Sq will be unaffected, meaning Sq can be continued separately.
If you register a step using then(...) on a sequence which is already currently complete, that step will be processed at the next opportunity. Otherwise, calls to then(...) will be queued up until that step is ready for processing.
You can register multiple steps, and multiple failure handlers. However, messages from a previous step (success or failure completion) will only be passed to the immediately next registered step (or the next failure handler). If you want to propagate along a message through multiple steps, you must do so yourself by making sure you re-pass the received message at each step completion.
To listen for any step failing, call or(...) (or alias onerror(..)) on your sequence to register a failure callback. You can call or() / onerror(..) as many times as you would like. If you call it on a sequence that has already been flagged as failed, the callback you specify will just be executed at the next opportunity.
ASQ(function(done){
done.fail("Failed!");
})
// could use `or(..)` or `onerror(..)` here
.or(function(err){
console.log(err); // Failed!
});
Gates
If you have two or more tasks to perform at the same time, but want to wait for them all to complete before moving on, you need a gate.
Calling gate(..) (or alias all(..) if you're from the Promises camp) with two or more functions creates a step that is a parallel gate across those functions, such that the single step in question isn't complete until all segments of the parallel gate are successfully complete.
For parallel gate steps, each segment of that gate will receive a copy of the message(s) passed from the previous step. Also, all messages from the segments of this gate will be passed along to the next step (or the next failure handler, in the case of a gate segment indicating a failure).
Example:
ASQ("message")
.all( // or `.gate(..)`
function(done,msg){
setTimeout(function(){
done(msg + " 1");
},200);
},
function(done,msg){
setTimeout(function(){
done(msg + " 2");
},10);
}
)
.val(function(msg1,msg2){
msg1; // "message 1"
msg2; // "message 2"
});
all(..) (or gate(..)) can also receive (instead of a function to act as a segment) just a regular asynquence sequence instance as a gate segment. When you call all(Sq), the Sq sequence is tapped immediately, but the success/error message streams of Sq will be unaffected, meaning Sq can be continued separately.
Handling Failures & Errors
Whenever a sequence goes into the error state, any error handlers on that sequence (or any sequence that it's been pipe()d to -- see Conveniences below) registered with or(..) will be fired. Even registering or(..) handlers after a sequence is already in the error state will also queue them to be fired (async, on the next event loop turn).
Errors can be programmatic failures (see above) or they can be uncaught JS errors such as ReferenceError or TypeError:
ASQ(function(done){
foo();
})
.or(function(err){
console.log(err); // ReferenceError: foo is not defined
});
In general, you should always register an error handler on a sequence, so as to catch any failures or errors gracefully. If there's no handlers registered when an error or failure is encountered, the default behavior of the sequence is to throw a global error (unfortunately not catchable with try..catch).
ASQ(function(done){
foo();
});
// (global) Uncaught ReferenceError: foo is not defined
However, there will be plenty of cases where you construct a sequence and fully intend to register a handler at a later time, or wire it into another sequence (using pipe() or seq()-- see Conveniences below), and these sequences might be intended to latently hold an error without noisily reporting it until that later time.
In those cases, where you know what you're doing, you can opt-out of the globally thrown error condition just described by calling defer() on the sequence:
var failedSeq = ASQ(function(done){
done.fail("Failed!");
})
// opt-out of global error reporting for
// this sequence!
.defer();
// later
ASQ(..)
.seq(failedSeq)
.or(function(err){
console.log(err); // Failed!
});
Don't defer() a sequence's global error reporting unless you know what you're doing and that you'll definitely have its error stream wired into another sequence at some point. Otherwise, you'll miss errors that will be silently swallowed, and that makes everyone sad.
Conveniences
There are a few convenience methods on the API, as well:
-
pipe(..)takes one or more completion triggers from other sequences, treating each one as a separate step in the sequence in question. These completion triggers will, in turn, be piped both the success and failure streams from the sequence.Sq.pipe(done)is sugar short-hand forSq.then(done).or(done.fail). -
seq(..)takes one or more functions, treating each one as a separate step in the sequence in question. These functions are expected to return new sequences, from which, in turn, both the success and failure streams will be piped back to the sequence in question.seq(Fn)is sugar short-hand forthen(function(done){ Fn.apply(null,[].slice.call(arguments,1)).pipe(done); }).This method will also accept asynquence sequence instances directly.
seq(Sq)is (sort-of) sugar short-hand forthen(function(done){ Sq.pipe(done); }). Note: theSqsequence is tapped immed
Related Skills
node-connect
337.3kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.2kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
337.3kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
83.2kCommit, push, and open a PR
