GithubHelp home page GithubHelp logo

getify / asynquence Goto Github PK

View Code? Open in Web Editor NEW
1.7K 70.0 149.0 515 KB

Asynchronous flow control (promises, generators, observables, CSP, etc)

JavaScript 99.31% HTML 0.69%
javascript async flow-control promises generators observables csp streams async-programming async-await

asynquence's Introduction

asynquence

CDNJS

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:


TL;DR: By Example

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 for Sq.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 for then(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 for then(function(done){ Sq.pipe(done); }). Note: the Sq sequence is tapped immediately, but the success/error message streams of Sq will be unaffected, meaning Sq can be continued separately.

    Additionally, this method can accept, either directly or through function-call, an Iterable Sequence. seq(iSq) is (sort-of) sugar short-hand for then(function(done){ iSq.then(done).or(done.fail); }).

  • val(..) takes one or more functions, treating each one as a separate step in the sequence in question. These functions can each optionally return a value, each value of which will, in turn, be passed as the completion value for that sequence step.

    val(Fn) is sugar short-hand for then(function(done){ done(Fn.apply(null,[].slice.call(arguments,1))); }).

    This method will also accept non-function values as sequence value-messages. val(Va) is sugar short-hand for then(function(done){ done(Va); }).

  • promise(..) takes one or more standard Promises/A+ compliant promises, and subsumes them into the sequence. See Promises/A+ Compliance below for more information.

    promise(Pr) is sugar short-hand for then(function(done){ Pr.then(done,done.fail); }).

    This method will also accept function(s) which return promises. promise(Fn) is sugar short-hand for then(function(done){ Fn.apply(null,[].slice.call(arguments,1)).then(done,done.fail); }).

  • fork() creates a new sequence that forks off of the main sequence. Success or Error message(s) stream along to the forked sequence as expected, but the main sequence continues as its own sequence beyond the fork point, and neither sequence will have any further effect on the other.

    This API method is primarily useful to create multiple "listeners" at the same point of a sequence. For example: Sq = ASQ()...; Sq2 = Sq.fork().then(..); Sq3 = Sq.fork().then(..); Sq.then(..). In that snippet, there'd be 3 then(..) listeners that would be equally and simultaneously triggered when the main Sq sequence reached that point.

    Note: Unlike most other API methods, fork() returns a new sequence instance, so chaining after fork() would not be chaining off of the main sequence but off the forked sequence.

    Sq.fork() is (sort-of) sugar short-hand for ASQ().seq(Sq).

  • duplicate() creates a separate copy of the current sequence (as it is at that moment). The duplicated sequence is "paused", meaning it won't automatically run, even if the original sequence is already running.

    To unpause the paused sequence-copy, call unpause() on it. The other option is to call the helper ASQ.unpause(..) and pass in a sequence. If the sequence is paused, it will be unpaused (and if not, just passes through safely).

    Note: Technically, unpause() schedules the sequence to be unpaused as the next "tick", so it doesn't really unpause immediately (synchronously). This is consistent with all other calls to the API (ASQ(), then(), gate(), etc), which all schedule procession of the sequence on the next "tick".

    The instance form of unpause(..) (not ASQ.unpause(..)) will accept any arguments sent to it and pass them along as messages to the first step of the sequence, each time it's invoked. This allows you to setup different templated (duplicated) sequences with distinct initial message states, if necessary.

    unpause() is only present on a sequence API in this initial paused state after it was duplicated from another sequence. It is removed as soon as that next "tick" actually unpauses the sequence. It is safe to call multiple times until that next "tick", though that's not recommended. The ASQ.unpause(..) helper is always present, and it first checks for an unpause() on the specified sequence instance before calling it, so that's safer.

  • errfcb is a flag on the triggers that are passed into then(..) steps and gate(..) segments. If you're using methods which expect an "error-first" style (aka, "node-style") callback, {trigger}.errfcb provides a properly formatted callback for the occasion.

    If the "error-first" callback is then invoked with the first ("error") parameter set, the main sequence is flagged for error as usual. Otherwise, the main sequence proceeds as success. Messages sent to the callback are passed through to the main sequence as success/error as expected.

    ASQ(function(done){ somethingAsync(done.errfcb); }) is sugar short-hand for ASQ(function(done){ somethingAsync(function(err){ if (err) done.fail(err); else done.apply(null,[].slice.call(arguments,1))}); }).

You can also abort() a sequence at any time, which will prevent any further actions from occurring on that sequence (all callbacks will be ignored). The call to abort() can happen on the sequence API itself, or using the abort flag on a completion trigger in any step (see example below).

API Static Functions

ASQ.failed(..) produces a sequence which is already in the failed state. If you pass messages along to failed(..), they will be the error messages for the sequence.

ASQ.messages(..) wraps a set of values as a ASQ-branded array, making it easier to pass multiple messages at once, and also to make it easier to distinguish a normal array (a value) from a value-messages container array, using ASQ.isMessageWrapper(..).

If you want to test if any arbitrary object is an asynquence sequence instance, use ASQ.isSequence(..).

ASQ.iterable(..) is added by the iterable-sequence contrib plugin. See Iterable Sequences below for more information.

ASQ.unpause(..) is a helper for dealing with "paused" (aka, just duplicated) sequences (see duplicate() above).

ASQ.noConflict() rolls back the global ASQ identifier and returns the current API instance to you. This can be used to keep your global namespace clean, or it can be used to have multiple simultaneous libraries (including separate versions/copies of asynquence!) in the same program without conflicts over the ASQ global identifier.

ASQ.clone() creates a fresh, clean copy of asynquence. This is primarily useful if you want to have different asynquence copies which are each extended with different plugins (see below).

Note: In node.js, if you load contrib bundle(s) from the standard top-level package location (./node_modules/asynquence-contrib/a-bundle-file.js), it will automatically look for and load (if found) the peer asynquence top-level package (./node_modules/asynquence/) and return it. So as a shortcut, you could simply do: var ASQ = require("asynquence-contrib") instead of loading both packages separately.

However, if you load contrib bundle(s) that cannot find a peer asynquence top-level package to load and use, a dependency-injection function is instead returned, which expects to be called with either an asynquence instance, or a relative path specifying where to load it.

In node, we can use the npm package freshy to let us reload the asynquence package to get a fresh copy of it, for each bundle to attach to:

var ASQ1 = require("./path/to/bundle1.js");

require("freshy").unload("asynquence");

var ASQ2 = require("./path/to/bundle2.js");

In the browser, you need to do something like this:

<script src="asq.js"></script>

<script>ASQ1 = ASQ.clone(); ASQ2 = ASQ.clone();</script>

<script>ASQ = ASQ1;</script>
<script src="./path/to/bundle1.js"></script>

<script>ASQ = ASQ2;</script>
<script src="./path/to/bundle2.js"></script>

Plugin Extensions

ASQ.extend( {name}, {build} ) allows you to specify an API extension, giving it a name and a build function callback that should return the implementation of your API extension. The build callback is provided two parameters, the sequence api instance, and an internals(..) method, which lets you get or set values of various internal properties (generally, don't use this if you can avoid it).

Example:

// "foobar" plugin, which injects message "foobar!"
// into the sequence stream
ASQ.extend("foobar",function __build__(api,internals){
    return function __foobar__() {
        api.val(function __val__(){
            return "foobar!";
        });

        return api;
    };
});

ASQ()
.foobar() // our custom plugin!
.val(function(msg){
    console.log(msg); // foobar!
});

The /contrib/ directory includes a variety of optional contrib plugins as helpers for async flow-controls. See these plugins for more complex examples of how to extend the asynquence API.

For browser usage, simply include the asq.js library file and then the contrib.js file. For node.js, these contrib plugins are available as a separate npm package: asynquence-contrib.

There are also other bundle options included with the npm package, such as contrib-es6.src.js and contrib-common.js. See Building Contrib Bundle for more information.

Iterable Sequences

One of the contrib plugins provided is iterable-sequence. Unlike other plugins, which add methods onto the sequence instance API, this plugin adds a new static function directly onto the main module API: ASQ.iterable(..). Calling ASQ.iterable(..) creates a special iterable sequence, as compared to calling ASQ(..) to create a normal asynquence sequence.

An iterable sequence works similarly to normal asynquence sequences, but a bit different. then(..) still registers steps on the sequence, but it's basically just an alias of val(..), because the most important difference is that steps of an iterable sequence are not passed completion triggers.

Instead, an iterable sequence instance API has a next(..) method on it, which will allow the sequence to be externally iterated, one step at a time. Whatever is passed to next(..) is sent as step message(s) to the current step in the sequence. next(..) always returns an iterator result object like:

{
    value: ...          // return messages
    done: true|false    // sequence iteration complete?
}

Note: If the value property is absent, it's assumed to be undefined, and if the done property is absent, it's assumed to be false.

value is any return message(s) from the next(..) invocation (undefined otherwise). done is true if the previously iterated step was (so far) the last registered step in the iterable sequence, or false if there's still more sequence steps queued up.

Just like with normal asynquence sequences, register one or more error listeners on the iterable sequence by calling or(..). If a step results in some error (either accidentally or manually via throw ..), the iterable sequence is flagged in the error state, and any error messages are passed to the registered or(..) listeners.

Also, just like next(..) externally controls the normal iteration flow of the sequence, throw(..) externally "throws" an error into the iterable sequence, triggering the or(..) flow as above. Iterable sequences can be abort()d just as normal asynquence sequences. You can also call return(..) (just like on normal iterators), which abort()s the sequence and returns an iterator result with the value passed in, if any, and done: true.

Iterable sequences are a special subset of sequences, and as such, some of the normal asynquence API variations do not exist, such as gate(..), seq(..), and promise(..).

function step(num) {
    return "Step " + num;
}

var sq = ASQ.iterable()
    .then(step)
    .then(step)
    .then(step);

for (var i=0, ret;
    (ret = sq.next(i+1)) && !ret.done;
    i++
) {
    console.log(ret.value);
}
// Step 1
// Step 2
// Step 3

This example shows sync iteration with a for loop, but of course, next(..) can be called in various async ways to iterate the sequence over time.

Iterable sequence steps can either be a function that produces a value, or a direct (non-function) value itself:

var sq = ASQ.iterable()
    .then(42)
    .then(function(x){
        return x * 2;
    })
    .then("hello world");

sq.next();      // { value: 42 }
sq.next(5);     // { value: 10 }
sq.next();      // { value: "hello world" }
sq.next();      // { done: true }

Just like regular sequences, iterable sequences have a duplicate() method (see ASQ's instance API above) which makes a copy of the sequence at that moment. However, iterable sequences are already "paused" at each step anyway, so unlike regular sequences, there's no unpause() (nor is there any reason to use the ASQ.unpause(..) helper!), because it's unnecessary. You just call next() on an iterable sequence (even if it's a copy of another) when you want to advance it one step.

Multiple parameters

API methods take one or more functions as their parameters:

  • gate(..) treats multiple functions as segments in the same gate.
  • The other API methods (then(..), or(..), pipe(..), seq(..), and val(..)) treat multiple parameters as just separate subsequent steps in the respective sequence. These methods don't accept arrays of functions (that you might build up programmatically), but since they take multiple parameters, you can use .apply(..) to spread an array of values out.

Promises/A+ Compliance

The goal of asynquence is that you should be able to use it as your primary async flow-control library, without the need for other Promises implementations.


If you're looking for actual Promises/A+ compliance, I've just released Native Promise Only, a tiny and fast polyfill of purely just the native ES6 Promise() mechanism.


asynquence is intentionally designed to hide/abstract the idea and use of Promises, such that you can do quick and easy async flow-control programming without some of the hassles/tedium of creating Promises directly.

As such, the asynquence API itself is not Promises/A+ compliant, nor should it be, because the "promises" used are hidden underneath asynquence's API. Note: the hidden promises behave predictably like standard Promises where they need to, so asynquence as an abstraction offers the same trust guarantees.

If you are also using other Promises implementations alongside asynquence, you can quite easily receive and consume a regular Promise value (or thenable) from some other method into the signal/control flow for an asynquence sequence.

For example, if using jQuery, the Q promises library, and asynquence:

// Using *Q*, make a standard Promise out
// of jQuery's Ajax (non-standard) "promise"
var p = Q( $.ajax(..) );

// Now, asynquence flow-control including a
// standard Promise
ASQ()
.then(function(done){
    setTimeout(done,100);
})
// subsume a standard Promise into the sequence
.promise(p)
.val(function(ajaxResp){
    console.log(ajaxResp);
});

Despite API similarities (like the presence of then(..) on the API), an asynquence instance is not itself designed to be used as a Promise value linked/passed to another standard Promise or other utilities that expect real promises.

Trying to do so will likely cause unexpected behavior, because Promises/A+ insists on problematic (read: "dangerous") duck-typing for objects that have a then() method, as asynquence instances do.

However, if you really need a standard native Promise from your sequence, you can use the toPromise contrib plugin, which vends/forks an actual native Promise off an asynquence sequence instance.

Browser, node.js (CommonJS), AMD: ready!

The asynquence library is packaged with a light variation of the UMD (universal module definition) pattern, which means the same file is suitable for inclusion either as a normal browser .js file, as a node.js module, or as an AMD module. Can't get any simpler than that, can it?

For browser usage, simply include the asq.js library file. For node.js usage, install the asynquence package via npm, then require(..) the module:

var ASQ = require("asynquence");

Note: The ASQ.noConflict() static function really only makes sense when used in a normal browser global namespace environment. It should not be used when the node.js or AMD style modules are your method of inclusion.

Usage Examples

Using the following example setup:

function fn1(done) {
    alert("Step 1");
    setTimeout(done,1000);
}

function fn2(done) {
    alert("Step 2");
    setTimeout(done,1000);
}

function yay() {
    alert("Done!");
}

Execute fn1, then fn2, then finally yay:

ASQ(fn1)
.then(fn2)
.then(yay);

Pass messages from step to step:

ASQ(function(done){
    setTimeout(function(){
        done("hello");
    },1000);
})
.then(function(done,msg1){
    setTimeout(function(){
        done(msg1,"world");
    },1000);
})
.then(function(_,msg1,msg2){ // basically ignoring this step's completion trigger (`_`)
    alert("Greeting: " + msg1 + " " + msg2);
    // 'Greeting: hello world'
});

Handle step failure:

ASQ(function(done){
    setTimeout(function(){
        done("hello");
    },1000);
})
.then(function(done,msg1){
    setTimeout(function(){
        // note the `fail` flag here!!
        done.fail(msg1,"world");
    },1000);
})
.then(function(){
    // sequence fails, won't ever get called
})
.or(function(msg1,msg2){
    alert("Failure: " + msg1 + " " + msg2);
    // 'Failure: hello world'
});

Create a step that's a parallel gate:

ASQ()
// normal async step
.then(function(done){
    setTimeout(function(){
        done("hello");
    },1000);
})
// parallel gate step (segments run in parallel)
.gate(
    function(done,greeting){ // gate segment
        setTimeout(function(){
            // 2 gate messages!
            done(greeting,"world");
        },500);
    },
    function(done,greeting){ // gate segment
        setTimeout(function(){
            // only 1 gate message!
            done(greeting + " mikey");
        },100);
        // this segment finishes first, but message
        // still kept "in order"
    }
)
.then(function(_,msg1,msg2){
    // msg1 is an array of the 2 gate messages
    // from the first segment
    // msg2 is the single message (not an array)
    // from the second segment

    alert("Greeting: " + msg1[0] + " " + msg1[1]);
    // 'Greeting: hello world'
    alert("Greeting: " + msg2);
    // 'Greeting: hello mikey'
});

Use pipe(..), seq(..), and val(..) helpers:

var seq = ASQ()
.then(function(done){
    ASQ()
    .then(function(done){
        setTimeout(function(){
            done("Hello World");
        },100);
    })
    .pipe(done); // pipe sequence output to `done` completion trigger
})
.val(function(msg){ // NOTE: no completion trigger passed in!
    return msg.toUpperCase(); // map return value as step output
})
.seq(function(msg){ // NOTE: no completion trigger passed in!
    var seq = ASQ();

    seq
    .then(function(done){
        setTimeout(function(){
            done(msg.split(" ")[0]);
        },100);
    });

    return seq; // pipe this sub-sequence back into the main sequence
})
.then(function(_,msg){
    alert(msg); // "HELLO"
});

Abort a sequence in progress:

var seq = ASQ()
.then(fn1)
.then(fn2)
.then(yay);

setTimeout(function(){
    // will stop the sequence before running
    // steps `fn2` and `yay`
    seq.abort();
},100);

// same as above
ASQ()
.then(fn1)
.then(function(done){
    setTimeout(function(){
        // `abort` flag will stop the sequence
        // before running steps `fn2` and `yay`
        done.abort();
    },100);
})
.then(fn2)
.then(yay);

Builds

The core library file can be built (minified) with an included utility:

./build-core.js

However, the recommended way to invoke this utility is via npm:

npm run-script build-core

License

The code and all the documentation are released under the MIT license.

http://getify.mit-license.org/

asynquence's People

Contributors

0xflotus avatar ajithbhat avatar getify avatar nolsherry avatar taytay avatar tomjnsn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

asynquence's Issues

Best way to open lots of files

I want to use Asynquence to take a list of globbed files and open them all to perform some operation and return a dictionary.

So I start out by globbing the directory to get these markdown files. That's step one.

function blog (req, res, next) {
  ASQ(getBlogPostFileNames)
}


function getBlogPostFileNames (done) {
  var options = { cwd: path.join(__dirname, '../blog') }

  glob('*.markdown', options, done.errfcb)
}

Now I'm stuck.

I could just map the array that I get, and nobody would mind, but I'm trying to think asynchronously. I have some number of operations to do, and I don't care about the order. Seems like the perfect use case for a gate (!)

The only way I can see to proceed is to create a separate sequence, which I have nothing against in principle, but I was wondering if I was missing some bit of the API that enables me to split an array out and turn it into a gate as a next step.

Something like:

ASQ(function multipleValues (done) {
  done([1, 2, 3])
}).each(function (done, val) {
  done(val * val)
}).val(function (/* args */) {
  console.log([].slice.call(arguments, 0))
})

externally iterable sequences (aka: asynquence "generators")

Normal asynquence sequences are internally iterable, that is, each step of the sequence gets its own completion trigger to advance the sequence iteration to the next step.

However, there's also a need for an asynquence sequence to be externally iterable. That is, have a sequence of steps set up, where the control-flow for advancing the sequence is outside the sequence. For instance, a for-of loop, or other such things, which advance a queue of steps.

The question is, should all sequences be both internally and externally iterable, or should there be two different kinds of sequences, one for each.

IOW:

var steps = ASQ()
.then(function(done){
   // step 1
})
.then(function(done){
  // step 2
});

For that sequence, should you be able to do something like:

steps.next();
steps.next();

The problem with that is if you externally advance a sequence and that step itself has a completion trigger, it's kind of a race to see who goes first, and you could accidentally advance.


By contrast, you could construct separate sequence types, for iterables, like:

var steps = ASQ.iterable()
.then(function(msg){
  // step 1
  // return a message instead of sending it on
})
.then(function(msg){
  // step 2
});

In this way, the iterable sequence is only controllable externally (no done triggers), so each step doesn't get a completion trigger as it normally would.

This would make the sequence basically like a generator (indeed it might end up someday implemented with them).

Instead of step 1 passing a message onto step 2 directly, it looks like:

var ret1 = steps.next("step 1");

steps.next(ret1.value); // pass the output message from step-1 onto step-2

For consistency/compatibility sake, the return value from next() would always be an object of { value: ..., done: true/false } format, just like with normal generators. The value is any return value from the function, and done is if the sequence currently has no more steps registered on it.

Obviously, an iterable sequence would not have all the other API methods/helpers that normal internal sequences do, such as gate(), seq() and val(), as they don't make any sense in this usage context. It would only have then() for steps, and or() for error handling.

Like generators, you could do steps.throw(..) to throw an error at the sequence at that current step. Or, inside a step, throwing an error (intentionally or not) would be caught and propagated into the or() handlers of the sequence.

Need to investigate best approach/design ideas for this. Thoughts welcomed.

Automatically detect and consume another asynquence

Consider:

var sq1 = ASQ(..)..;

ASQ()
.then(..)
.seq(function(){
   return sq1;
})
..

The seq(function(){ return sq1; }) syntax is needlessly verbose, and should have a short-hand, perhaps like:

var sq1 = ASQ(..)..;

ASQ()
.then(..)
.seq(sq1)
..

Since seq(..) expects a function, if it instead gets an object, we could probably safely assume that the object is an asynquence object, and just consume it (pipe its output and error streams) automatically, instead of requiring the function execution indirection.


Only issue to resolve: seq(..) would detect an object and assume an asynquence, and Issue #5 suggests that both ASQ(..) and val(..) will assume a non-function parameter as an auto-fulfilled step message.

Should ASQ(..) detect an asynquence as a special case (using branding or duck-typing)? Should:

var sq1 = ASQ(..)..;

ASQ(sq1)
.then(..)
..

... be legal? In that specific case, you could always do this, instead:

var sq1 = ASQ(..)..;

sq1
.then(..)
..

So, maybe ASQ(..) doesn't need to special-case detect. Hmmm.....

Improvement: `map(..)` getting `arr` and/or `each` from value-message stream

Per this gist from @EdJ, map(..) should be able to detect if it's missing either arr or each argument or both, and if so, attempt to pull them from the stream of value-messages passed from the previous step.

For instance, this should work:

ASQ()
.then(function(done){
   done([1,2,3]); // provide the `arr` for `map(..)` to iterate over
})
.map(function(item,done){
   done(item+1);
})
.val(function(arr){
   console.log(arr); // [2,4,6]
});

Even if just map() is called on a chain with no args at all, it should attempt to find both the arr and each from the value message stream.

Note: it should still pass any additional stream messages onto the each callback itself, like:

ASQ()
.val(function(){
   return ASQ.messages([1,2,3],4);
})
.map(function(item,done,msg){
   done(item * msg);
})
.val(function(arr){
   console.log(arr); // [8,16,24]
});

appendable queue

Is it possible to fit asynquence to the use case described below?

  1. There is a queue :)
  2. Few arbitrary events listener(s) catch(es) some events and append(s) events-based (promises of) messages to the queue one by one.
  3. There is a handler attached to the queue processing incoming messages one by one for side effects.

In fact it is close to actor model.

Can a runner drive a sequence synchronously instead of async?

Right now, if I use runner on a sequence like this:

ASQ()
.runner(ASQ.iterable()
.then((theToken)=> {
var currentTime = Date.now();
return currentTime;
})
.then((lastTime)=>{
var currentTime = Date.now();
console.log("elapsed = "+(currentTime - lastTime));
return currentTime;
})
.then((lastTime)=>{
var currentTime = Date.now();
console.log("elapsed = "+(currentTime - lastTime));
return currentTime;
})
.then((lastTime)=>{
var currentTime = Date.now();
console.log("elapsed = "+(currentTime - lastTime));
return currentTime;
})
.then((lastTime)=>{
var currentTime = Date.now();
console.log("elapsed = "+(currentTime - lastTime));
return currentTime;
}));

elapsed = 5
elapsed = 5
elapsed = 4
elapsed = 5

If I drive the same sequence synchronously (with a for loop), it obviously doesn't pay the penalty for putting the next run on the queue. Is there something fundamental about the architecture of runner that would prevent me from making a runner call the next step synchronously if the next step was ready to run? (In other words, if we weren't waiting on a promise or another async construct).

Contrib: waterfall(..)

Need a waterfall(..) contrib plugin, which takes a list of functions, and applies them in order just like a normal sequence of then(..)s, but it collects aggregated output, where step 2 gets output from step 1, step 3 gets output from both step 2 and 3, step 4 gets messages from 1-3, etc.

The final success message(s) from waterfall(..) is the full aggregate of all success messages. An error anywhere along the way behaves like an error in a normal sequence.

Note: this method is sorta conceptually similar to an array-map, where we're mapping a set of steps to a set of messages.

research adding a "co" API method/plugin for CSP-style coroutines

Having two+ generators, or iterable-sequences, which are cooperatively concurrent, and message each other as they process through their steps. Imagining something like:

ASQ()
.co(
   function*(){
      var x = yield 2;
      x = yield (x * 2);
      console.log(x); // 8
   },
   function*(){
      var y = yield 6;
      y = yield (y * 4);
      console.log(y); // 12
   }
)
.val(function(){
   console.log("coroutines done");
});

for `runner()` plugin, make a test for actual generators

This is a tricky one because generators are invalid syntax and have to be feature-detected using Function or eval, as well as the running of a test for them. Need to figure out how to take a test and get it into a string so the test suite can run the test only in environments where a generator is valid.

make iterable-sequences **actually** iterable (per ES6 for-of)

The goal is to be able to do:

var isq = ASQ.iterable()
.then(function(){ return 5; })
.then(function(){ return 10; })
.then(function(){ return 15; });

for (var v of isq) {
   console.log(v);
}
// 5   10   15

This example code makes a custom iterator for an object. It works in FF nightly. Need to research how to properly feature-detect this stuff and include something like it in the iterable-sequence contrib plugin so it returns itself when asked for its @@iterator. Probably needs a future-proof Symbol.iterator feature detect in there too.

const iterator = (function() {
  try {
    for (var _ of Proxy.create({get: function(_, name) { throw name; } }))
      break;
  } catch (name) {
    return name;
  }
  throw 'wat';
})();

var mine = {};

// construct a fake iterator that just spits out the series of integers from 1-5
mine[iterator] = function() { return {next:function(){
    mine.count = mine.count || 0;
    mine.count++;
    var ret = { done: false, value: mine.count };
    if (mine.count > 5) ret.done = true;
    return ret;
}}};

for (var x of mine) {
    console.log(x);
}
// 1  2  3  4  5

References:

`ASQ.isMessageWrapper(..)` should check for actual array

ASQ.isMessageWrapper(..) currently only checks for ASQ branding, which means it's incapable of distinguishing between an ASQ instance and a message wrapper. Should fix the check so that it looks for Array.isArray(..) to match the tested object as well.

React needs disposal of events

In the example using react, I see the following code:

ASQ.react(
   // this listener setup handler will be called only once
   function(proceed){
      // we can call `proceed(..)` (or whatever you want to call the param!)
      // every time our stream/event fires, instead of just once, like
      // normal promise triggers
      $("#button").click(function(evt){
         // fire off a new sequence for each click
         proceed(this.id);
      });
   }
)
// each time our reactive event fires, process the rest of this sequence
.then(..)
.seq(..)
.then(..)
.val(..);

The problem with this code is that there's explicit adding of an event, but at no point is there ever a removal of said event when the sequence comes to an end, which leads to leaking event handlers.

`fork()` off a sequence

Given a sequence sq, sq.fork() would create a new sequence which has as its first step a "listener" to the main sq sequence at whatever point in the chain fork() is called. The new sequence would have its next subsequent step sent any output (success or error) once the main sq step reaches that point.

Since fork() returns a new sequence, you can't call fork().fork()... on the main chain, as the second fork() would be off the new sequence and would thus create a third sequence, etc. However, var a = sq.fork(); var b = sq.fork(); ... would give you the ability to fork the main sequence as many times as desired.

var sq = ASQ(..).then(..).val(..).seq(..)...;

var sq2 = sq.fork().then(..).then(..).gate(..)...;
var sq3 = sq.fork().val(..).seq(..)...;

fork() also needs to work off of iterable-sequences (producing another iterable-sequence), where calling isq.next() at a point where there's one or more forked iterable-sequences listening/waiting would essentially trigger a next() call on not only the main isq iterable-sequence, but all the forked sequences off that fork point as well.

`seq( func )` or `promise( func )` isn't re-executing `func` on duplicated sequence

This is kinda a bizarre oops.

Reproduce code:

var count = -1;

var a = ASQ()
.seq(function(){
    return ASQ(function(done){
        count++;
        if (count === 0) done.fail(count);
        else done(count);
    });
})
.val(function(msg){ console.log("success:" + msg); })
.or(function(err){ console.log("err:" + err); });

var b = a.duplicate();
b.unpause();

Should output "err:0" and "success:1", but since the seq( .. ) function callback isn't being re-executed, "err:0" is printed twice. :(

Auto-fulfilled step with message(s)

Just like with promises, pass any value other than function to ASQ, get automatic message passing to next step.

ASQ(3)
.val(function(msg){
   console.log(msg); // 3
});

This is helpful short-hand for:

ASQ()
.val(function(){
   return 3;
})
.val(function(msg){
   console.log(msg); // 3
});

Also, allow multiple values to ASQ() as multiple messages:

ASQ(3,"foo")
.val(function(msg1,msg2){
   console.log(msg); // 3 foo
});

Also, let this behavior happen to .val(..) as well, so this kind of message injection can happen in the middle of a chain:

ASQ(doSomething)
.val(3,"foo")
.then(function(done,msg1,msg2){
   console.log(msg1,msg2); // 3 foo
});

API naming

asynquence looks interesting has a lot of potential I think. The API looks very clean and concise. There are a few minor things that hurt my eye though:

  • The abbreviation "ASQ" doesn't look very friendly and accessible to me and for some reason wards me off. This may be personal though. How about just the character "A"?
  • The function name for adding an error handler to the sequence is or. I find this very confusing. When I see or in the context of sequences/flows, I think about "doing this or that", "doing any of the two". The name or has really nothing to do with errors. I would rename this to catch, which everybody will understand immediately.
  • The method gate has an alias all. I personally find the name all way more intuitive. There is nothing wrong with reusing familiar dictionary from Promises if you ask me :). Anyhow, please choose one of the two and remove the alias. I don't see a reason to have two functions doing the same thing here, and it is important to keep the API as concise and consistent as possible. Aliases can easily lead to confusion, you will end up seeing both used mixed in code and on the web, people may wonder if there is a difference between the two and get confused - totally unnecessary.

Just my 2 cents to make the API even better...

sugar to handle node-style callbacks

[Update]: I finally settled on errfcb, so I updated this description to reflect

Inspired from: https://github.com/then/promise#promisedenodeifyfn and thanks to @julienw's tweet.

Two suggestions to consider (either/both):

  1. The done trigger provided to then(..) callbacks should have a flag on it called errfcb: ASQ().then(function(done){ foo( "bar", done.errfcb ); }) which automatically detects the node-style callback err parameter and calls done.fail(err), otherwise just calls done(..) directly.
  2. More intrusive, but the main sequence API could have errfcb() on it, which returns not a chain continuation of the sequence as normal, but rather a node-style callback that continues the sequence: sq = ASQ()....; foo( "bar", sq.errfcb() )

The first feels like something to just roll directly into asynquence core, and the second feels more like something to put into contrib.

Other thoughts welcome.


So, it's probably gonna look like this:

ASQ()
.gate(
   function(done){
      fs.readFile( "foobar.txt", done.errfcb );
   },
   function(done){
      fs.readFile( "bazbam.txt", done.errfcb );
   }
)
.then(function(done, foobar, bazbam){
   fs.writeFile( "both.txt", foobar + bazbam, done.errfcb );
})
...

-or-

var sq = ASQ();

fs.readFile( "foobar.txt", sq.errfcb() );
fs.readFile( "bazbam.txt", sq.errfcb() );

sq.then(function(done, foobar, bazbam){
   fs.writeFile( "both.txt", foobar + bazbam, done.errfcb );
})
...

sync vs. async resolution of the sequence steps

Investigate making this pattern not result in nested timeouts (which is then subject to clamping):

https://gist.github.com/getify/5d5e0090cf446248c724#file-gistfile2-js

Idea for how: https://twitter.com/juandopazo/status/365482783122010115

BTW, this would also imply that the chain executes all subsequent but waiting steps in the current "event turn" synchronously, instead of always deferring each next step via timeouts, which further avoids the nested timeouts-clamping thing.

[thread moved]: asynquence error handling and API pros/cons

[Editor: I have moved part of a thread from another repo, started by @benjamingr, here since it deals directly with asynquence]

Consider this:

myService().then(function(){
doSomething(); // may throw an error, e.g. JSON.parse
});

Most promise libraries include a way to debug promise based code without having to > always add a .catch(function(e){ throw e}) handler, some examples:

  • Native promises in Firefox use garbage collection in order to track unhandled rejections > and log the errors to the console.
  • Libraries like Q use .done to signal that a chain is terminated and rejections that got to that point log.
  • Bluebird promises track unhandled rejection approximately (in a way that practically always works).

make a plugin which vends a native promise off a sequence

For the case where you have an asynquence sequence and want to vend a native promise that's chained to the sequence, create an API method via an optional contrib plugin.

For example:

ASQ()
.then(function(done){
   setTimeout(function(){
      done(2);
   },1000);
})
.toPromise()
.then(function(msg){
   console.log(msg); // 2
});

runner with multiple generators: make it more CSP-like

Right now, the runner(..) plugin uses a dummy-simple round-robbin scheduling to "cooperate" between multiple generators. It also uses a dummy-simple messaging mechanism to pass along one yielded value to the next generator and back, etc.

A more sophisticated mechanism is called for. One more like (though not necessarily identical to) the one detailed here: http://swannodette.github.io/2013/08/24/es6-generators-and-csp/

I'm envisioning something like:

// just a silly number generator that's promise-async
function getVal() {
   return new Promise( function(resolve,reject){
      setTimeout( function(){
         if (!getVal.__number) getVal.__number = 0;
         resolve( getVal.__number += 10 );
      }, 100 );
   } );
}

ASQ( 2, 3 )
.runner(
   // co-rountine 1
   function*(token){
      token.messages; // [2, 3]

      token.messages[0] += yield getVal(); // pauses while promise resolves
      token.messages[1] += yield getVal(); // pauses while promise resolves
      token.messages.push( token.messages[0] + token.messages[1] );

      token.messages; // [12, 23, 35]

      yield token; // hand control over to next co-rountine

      token.messages; // [ 100 ]

      token.messages[0] += yield getVal(); // pauses while promise resolves

      // implicit finish means transfer control to next co-rountine, if any
   },
   // co-rountine 2
   function*(token){
      token.messages; // [12, 23, 35]

      token.messages = [
         token.messages[0] + token.messages[1] + token.messages[2]
      ];
      token.messages; // [ 70 ]

      token.messages[0] += yield getVal(); // pauses while promise resolves
      token.messages; // [ 100 ]

      yield token; // hand control over to next co-rountine

      token.messages; // [ 140 ]

      token.messages.push( "Hello World" );

      // no magic: explicitly yield `token.messages`, or any other direct value
      // if desired. if you don't explicitly yield something non-undefined at the
      // end, the last non-`undefined`, non-`token` value yielded will be the
      // outbound message(s) from this run
      yield token.messages;
   }
)
.val( function(msgs){
   console.log( msg[0], msgs[1] ); // 140 "Hello World"
} );

OK, so as I wrote that out, I kinda like it even more.

Basically each co-rountine (aka generator) is given a token with a messages "channel" (array). You can add/remove from the channel as you see fit. You can even yield your current generator with a promise and have it restarted as many times as you see fit. If however you actually yield token (or something that results in it), then you are explicitly transferring control to the next co-rountine. Ordering of control is still simple round-robbin.

Thoughts?

Would be especially interested to hear any reactions/criticisms from @swannodette. :)

`runner` to run/iterate over iterable-sequences or generators

A contrib plugin called runner that adds runner(..) to the sequence API.

This function (inspired by spawn and others like it) takes either an iterable-sequence or a generator (that must yield sequences or thennable-promises), and runs the iteration to completion (if any).

Examples:

function double(x) {
    if (!x) x = 1;

    return ASQ(function(done){
        setTimeout(function(){
            done(x * 2);
        },500);
    });
}

ASQ(2)
.runner(
    ASQ.iterable()
    .then(double)
    .then(double)
    .then(double)
    .then(double)
)
.val(function(msg){
    console.log("finished value: " + msg);
    // finished value: 32
});

Or:

function double(x) {
    return ASQ(function(done){
        setTimeout(function(){
            done(x * 2);
        },500);
    });
}

ASQ(2)
.runner(function*(x){
    if (!x) x = 1;

    while (x < 32) {
        // yields sequences
        x = yield double(x);
    }

    yield x;
)
.val(function(msg){
    console.log("finished value: " + msg);
});

Or:

function double(x) {
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            resolve(x * 2);
        },500);
    });
}

ASQ(2)
.runner(function*(x){
    if (!x) x = 1;

    while (x < 32) {
        // yields standard promises
        x = yield double(x);
    }

    yield x;
})
.val(function(msg){
    console.log("finished value: " + msg);
});

provide optional "legacy.js" file

Provide an optional "legacy.js" file to use if loading lib in old/crappy browser. Include the ES5 shims (or anything else) the lib needs to ensure it works properly.


Research if there's a way for the build-processes (for asynquence and asynquence-contrib) to scan/parse the files for such usage and automatically build "legacy.js". Probably needs its own tool to do that well.

Explore how to duplicate a sequence

Given a sequence like:

var sq = ASQ(..).then(..).seq(..).val(..);

Would it be possible to duplicate a sequence, perhaps like:

var sq_template = sq.duplicate();

The duplicated sequence would probably need to start out "paused", perhaps as a mixture of iterable-sequences and normal sequences, where the first step can be externally advanced with a next() or start() or something, and the rest of the sequence would be a normal internally iterable sequence.

One way this might be useful:

// restart the sequence to recover from an error
sq.or(function recover(err){
   // swqp out to saved "backup" template
   sq = sq_template;

   // start up the new sequence
   sq.start(); // or iterable-sequence `next()`

   // now, make another "backup" template
   sq_template = sq.duplicate();

   // setup the new sequence to error recover automatically
   sq.or(recover);
});

We could perhaps even have a helper:

// restart the sequence to recover from an error
sq.or(function recover(err) {
   sq = ASQ.restart(sq_template);

   // now, make another "backup" template
   sq_template = sq.duplicate();

   // setup the new sequence to error recover automatically
   sq.or(recover);
});

Extending a local instance of ASQ instead of the global one

I was wondering if there is a way to extend a local instance of ASQ rather than polluting the global ASQ instance. If I'm right, this global ASQ instance is shared by by every module in your project using ASQ (including third party libraries), so it's possible to get conflicts there (especially when ASQ becomes widely used...).

Gate example in README does not work.

I'm trying to use the gate example in the README to learn how to pass messages (debugging #2). However, the messages don;t seem to get passed to the gate segments.

Here is the code. It is almost an exact copy of the example in the README.

var ASQ = require('asynquence');                                                                                                                                                              

ASQ()                                                                                                                                                                                         
// normal async step                                                                                                                                                                          
.then(function(done){                                                                                                                                                                         
    setTimeout(function(){                                                                                                                                                                    
        done("hello");                                                                                                                                                                        
    },1000);                                                                                                                                                                                  
})                                                                                                                                                                                            
// parallel gate step (segments run in parallel)                                                                                                                                              
.gate(                                                                                                                                                                                        
    function(done,msg1){ // gate segment                                                                                                                                                      
        setTimeout(function(){                                                                                                                                                                
            done(msg1,"world");                                                                                                                                                               
        },500);                                                                                                                                                                               
    },                                                                                                                                                                                        
    function(done,msg1){ // gate segment                                                                                                                                                      
        setTimeout(function(){                                                                                                                                                                
            done(msg1,"mikey");                                                                                                                                                               
        },100); // segment finishes first, but message still kept "in order"                                                                                                                  
    }                                                                                                                                                                                         
)                                                                                                                                                                                             
.then(function(_,msg1,msg2){                                                                                                                                                                  
    console.log("Greeting: " + msg1[0] + " " + msg1[1]); // 'Greeting: hello world'                                                                                                           
    console.log("Greeting: " + msg2[0] + " " + msg2[1]); // 'Greeting: hello mikey'                                                                                                           
});          

Output:

Greeting: undefined world
Greeting: undefined mikey

Am I going crazy, or is something wrong here?

Node 0.10.9
ASQ 0.1.0

What happens to aborted sequences?

If you seq() or pipe() sequence "A" into sequence "B", but sequence "A" gets aborted (or is already aborted), should sequence "B" know about it, or just hang forever waiting for a notification it will never get?

For context, if "A" were error'd out, "B" would hear about it through the error stream (or() chain).

But currently, and aborted sequence doesn't make that fact known to anyone, even another sequence that's listening in on him, so "B" just hangs. Dunno if this should be considered desired or bad/buggy.

Hmmm....

var sq1 = ASQ()
.then(function(done){
   // abort 500ms from now!
   setTimeout(done.abort,500);
});

ASQ()
.then(function(done){
   // continue 1000 ms from now!
   setTimeout(done,1000);
})
// now, let's wait on `sq1`, futily
.seq(sq1)
.then(function(done){
   // never gets called because `sq1` silently aborted and this sequence can't tell!
   console.log("This will never happen");
});

One possible solution, partial anyway, is that if you try to seq() or pipe() off an already-aborted sequence, THAT could throw an error into your existing sequence. That would seem helpful.

But what if you are already listening to the sequence when it aborts itself? A sequence can't tell if it is being listened to by another sequence, so it would have no way to know that it ought to throw up an error. And I don't want errors thrown on abort, since abort is designed to stop the sequence in its tracks, dead.

Maybe there needs to be an "abort" channel/stream, in addition to the "success" and "error" streams, so that silently, whenever any part of a sequence or sequence-chain is aborted, the whole thing aborts itself all the way up.

So, should the second ASQ sequence above:

  1. continue normally (listen for, then ignore the abort for sq1)?
  2. abort itself too once sq1 is aborted (either before or after we listen in)?
  3. hang (as it currently does)?

contrib: return ASQ as "public API" of contrib plugin

Since contrib wrapper has no public API of its own, just return ASQ. Could make it easy or canonical to just do:

var ASQ = require("asynquence-contrib");

Instead of:

var ASQ = require("asynquence");
require("asynquence-contrib");

Note: need to verify that _asynquence-contrib_s require("asynquence") is indeed pulling in the same global asynquence, and not its own dependency, which could be causing version mis-match if so.

Multiple messages wrapper

Allow multiple messages to be wrapped in a type-identifiable container (not just an array, but array-like), mostly so val(..) handlers can return multiple messages without array overlap.

ASQ()
.val(function(){
   return ASQ.messages(1,2,3);
})
.val(function(msg1,msg2,msg3){
   console.log(msg1,msg2,msg3); // 1 2 3
});

This is short-hand for doing:

ASQ(function(done){
   done(1,2,3);
})
.val(function(msg1,msg2,msg3){
   console.log(msg1,msg2,msg3); // 1 2 3
});

ASQ.messages(..) wrapper would also be useful for throwing multiple error messages:

ASQ(function(done){
   throw ASQ.messages(1,2,3);
})
.or(function(err1,err2,err3){
   console.log(err1,err2,err3); // 1 2 3
});

And ASQ.messages(..) wrapper would automatically be the new type (though it's still array-like) wrapper of message passed in when you have multiple messages sent from a gate segment.

ASQ()
.gate(
   function(done){ done(1,2,3); },
   function(done){ done(4); }
)
.val(function(msgs1,msg2){
   console.log(Array.isArray(msgs1)); // true
   console.log(msgs1.__ASQ__); // true
   console.log(msgs1); // [1,2,3]

   console.log(msgs2); // 4
});

"Reactive Sequences" (asynquence's "reactive" pattern)

This idea exploration comes from a conversation with @mgonto. Almost certainly relies on #29.

We can already do something like:

$("#button").click(function(evt){
   ASQ(this.id)
   .then(..)
   .seq(..)
   .then(..)
   .val(..)
});

In that way, we create a new sequence to handle each incoming "event" from a "stream", which is sorta the spirit of how Reactive observables work.

The question is, could we make first-class support for something like this pattern:

ASQ.react(
   // this react/listen handler will be called only once
   function(proceed){
      // we can call `proceed(..)` (or whatever you want to call the param!)
      // every time our stream/event fires, instead of just once, like
      // normal promise triggers
      $("#button").click(function(evt){
         // fire off a new sequence for each click
         proceed(this.id);
      });      
   }
)
// each time our reactive event fires, process the rest of this sequence
.then(..)
.seq(..)
.then(..)
.val(..);

Calling gate with an array of functions

I want to dynamically create an array of functions to be executed in parallel. How can I pass that to gate to execute? I'm not sure I'm doing it correctly. I think I might need to use .seq() here like this:

    .seq(function(endpoints) {   // for this example, endpoints is an array of two object.                                                                                                                                                          
        var calls = [];                                                                                                                                                                   
        endpoints.forEach(function(endpoint) {                                                                                                                                            
            calls.push(function(done) {                                                                                                                                                   
                endpoint.getConfig(function(err, config) {                                                                                                                                
                    if (err) {                                                                                                                                                            
                        console.log(err);                                                                                                                                                 
                    }                                                                                                                                                                     
                    config.id = endpoint.id;                                                                                                                                              
                    console.log(config.id);                                                                                                                                               
                    done(config);                                                                                                                                                         
                });                                                                                                                                                                       
            });                                                                                                                                                                           
        });                                                                                                                                                                               
        var seq = ASQ();                                                                                                                                                                  
        seq.gate.apply(this, calls);                                                                                                                                                      
        return seq;                                                                                                                                                                       
    })                                                                                                                                                                                    
    .then(function(done, configs) {                                                                                                                                                       
        console.log(configs);                                                                                                                                                             
        var configMap = _.pluck(configs, 'id');                                                                                                                                           
        console.log(configMap);                                                                                                                                                           
        done();                                                                                                                                                                           
    }); 

I believe that configs in that final .then() should be an array, but it is just a single object. Am I not using .gate() and .seq() right here?

API extras

Add optional plugins/add-ons/extras/helpers/etc package that provides some additional convenience APIs:

Gate variations:

  • all(..) is an alias of gate(..) (for those who enjoy similarity with Promises)
  • any(..) is like gate(..), except just one segment has to succeed to proceed on the main sequence.
  • first(..) is like any(..), except as soon as any segment succeeds, the main sequence proceeds (ignoring subsequent results from other segments).
  • last(..) is like any(..), except only the latest segment to complete successfully sends message(s) along to the main sequence.
  • none(..) is the inverse of gate(..): the main sequence proceeds only if all the segments fail (with all segment error message(s) transposed as success message(s) and vice versa).

Sequence-step variations:

  • until(..) is like then(..), except it keeps re-trying until success or break() (for loop semantics) before the main sequence proceeds.
  • try(..) is like then(..), except it proceeds as success on the main sequence regardless of success/failure signal. If an error is caught, it's transposed as a special-format success message: { catch: ... }.

This also implies a "plugins" functionality where some internals hooks can be provided (specifically the ability to register something to be injected into each sequence API instance).

Now errors are reported too aggressively

In contrast to #35, there's lots of patterns where you legitimately know that it's ok for an error to not be forced as a thrown error, because you will eventually pipe that sequence into another one (so it won't be lost).

We need a way to mark a sequence so it defers its error reporting.

Note: This is basically the reverse of what others in the promise world are doing, by adding a done() that lets you mark a promise as not being further chained, and so any latent errors should be thrown. Instead of opting into aggressive error reporting, we'll offer an opt-out.

should contrib's `wrap(..)` pass-thru `this` binding?

Right now, wrap(..) forces the this binding of its passed fn function to be ΓΈ (an empty DMZ object). So, if you need to wrap a this-aware function (aka "method"), you have to call .bind(..) yourself at time of wrapping:

var something = ASQ.wrap( obj.methodName.bind(obj) );

something(..);

If you forget, you lose your this binding, and things go bummer. Would it be less of a footgun if we passed through the this binding?

var something = ASQ.wrap( obj.methodName );
var something2 = ASQ.wrap( obj.methodName );

something(..); // fails
something.call(obj, ..); // works

something2 = something2.bind(obj);
something2(..); // works

obj.something = something;
obj.something(..); // works

Not sure if this feels better or more footgun'ish. It does preserve the flexibility to have an asynquence-wrapped function that can still be bound to another this later if needed.


I don't really want to complicate the API with another utility that handles this binding, but we could do this I guess:

var something = ASQ.wrap( obj.something, { this: obj } );

something(..); // works

Even something else we could do is make that binding be a "soft binding", where it defaults, but lets you override:

var something = ASQ.wrap( obj.something, { this: obj } );

something(..); // works against `obj`
something.call( obj2, .. ); // also works, but against `obj2`

That feels the most flexible and useful. But is it? Thoughts?

`map(..)` to async map array items to new values

Per this twitter conversation and inspired by async.map(..), add map(arr, eachFn) to asynquence, which asynchronously maps values from arr into new values by calling eachFn(..) for each value.

The eachFn(..) function is invoked with item and doneTrigger parameters, as well as any previous-step sequence messages (just like with normal gates). doneTrigger(..) should be called with the new value(s) for the respective array item.

The final sequence message to come out of a map(..) should be the newly constructed array of mapped values.

A Typescript definitions file would be great

I'll be using asynquence in Typescript in addition to Javascript.

A definition file is a header file. A file like this (asynquence.d.ts) would tell Typescript what the asynquence API looks like, so that it can auto-complete, give compiler errors if you pass the wrong type, leave off a parameter, etc. It will obviously take some familiarity with Typescript to write one. There are definition files for most popular frameworks/libraries: DefinitelyTyped project.

I will start playing with generating one, and will post back here if I have much luck.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.