2

I didn't know how to appropriately write the title of this question but i will explain my problem:

Imagine there is a list with some data, for example: ["first", "second", "third"]

Also there is a AJAX call that does something to it's argument, for example:

function ajax(data){
  return new Promise(function (resolve, reject) {
    setTimeout(() => resolve(data+"1"), 2000)
  });
}

And for every AJAX call you need a follow-up action, for example:

ajax(e).done(data => d.resolve(data+"2"));

Now i want to make the AJAX call and the follow-up action asynchronously for every item of the list but want to wait (non blocking) until every item is finished.

For a solution i want to use generators and the co library.

Only running the AJAX call asynchronously for every list item works great:

var consoleLine = "<p class=\"console-line\"></p>";

console = {
    log: function (text) {
        $("#console-log").append($(consoleLine).html(text));
    }
};

co(function*(){
  let res = yield ["first", "second", "third"].map(e => ajax(e));
  res.forEach((a, b, c) => console.log(a));
});

function ajax(data){
  return new Promise(function (resolve, reject) {
       setTimeout(() => resolve(data+"1"), 2000)
  });
}
.console-line
{
    font-family: monospace;
    margin: 2px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://raw.githubusercontent.com/tj/co/master/index.js"></script>

<div id="console-log"></div>

But running it with the follow-up action doesn't work:

var consoleLine = "<p class=\"console-line\"></p>";

console = {
    log: function (text) {
        $("#console-log").append($(consoleLine).html(text));
    }
};

co(function*(){
  let res = yield test(["first", "second", "third"]);
  res.forEach((a, b, c) => console.log(a));
});

function test(g) {
  return g.map(e => function(){
    let d = new $.Deferred();
    ajax(e).done(data => d.resolve(data+"2"));
   return d.promise();
  });
}

function ajax(data){
  return new Promise(function (resolve, reject) {
       setTimeout(() => resolve(data+"1"), 2000)
  });
}
.console-line
{
    font-family: monospace;
    margin: 2px;
}
<script src="https://raw.githubusercontent.com/tj/co/master/index.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="console-log"></div>

Why is that? How to get my requirement to work?

Spenhouet
  • 6,556
  • 12
  • 51
  • 76
  • Avoid the [deferred antipattern](http://stackoverflow.com/q/23803743/1048572)! – Bergi Aug 21 '16 at 10:53
  • What happens when it "doesn't work"? I cannot reproduce here. – Bergi Aug 21 '16 at 10:53
  • @Bergi: It stops at the `yield` and doesn't execute `res.forEach`. Somehow the deffered didn't work but the promise from `.then` did (see my answer below). – Spenhouet Aug 21 '16 at 11:12
  • Oh, I see your problem now. – Bergi Aug 21 '16 at 11:23
  • ES6 promises offer you to write asnyc code like sync in a functional manner while when promises teamed up with generators you can do the same thing in an imperative fashion. Have a look at this page https://curiosity-driven.org/promises-and-generators – Redu Aug 21 '16 at 11:31

4 Answers4

1

Now i want to make the AJAX call and the follow-up action asynchronously for every item of the list but want to wait (non blocking) until every item is finished.

Sounds like you want to wait for all the promises to fulfill. This is what Promise.all() and $.when are good for.

var data = ["first", "second", "third"];

function ajax(data){
  let d = new $.Deferred();
  setTimeout(() => d.resolve(data+"1"), 1000);
  return d.promise();
}

function followUpAction(data){
  console.log('following', data);
}

var promises = data.map(function(item) {
  return ajax(item).then(followUpAction);
});

$.when.apply($, promises).then(function() {
  console.log('all done');
});
rodneyrehm
  • 13,442
  • 1
  • 40
  • 56
  • thank you for your help. I do want to use generators and therefor your solution doesn't fit my need, but you pointed me to the problem why it wasn't working. – Spenhouet Aug 21 '16 at 11:05
1

Here's your problem:

return g.map(e => function(){
//             ^^ ^^^^^^^^^^
    let d = new $.Deferred();
    ajax(e).done(data => d.resolve(data+"2"));
    return d.promise();
});

That's an arrow function returning a function expression. You either want

return g.map(function(e) {

or

return g.map(e => {

to make it work, otherwise you only get back an array of functions (and co will treat that in weird ways).


And yes, you definitely should use then instead of done + deferreds.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • You are right! I somehow didn't see that i'm just returning a function (with promise as return value). Thank you for pointing this out. – Spenhouet Aug 21 '16 at 11:30
0

I want to use generators and therefor rodneyrehm's answer doesn't solve my need. But his answer did help me solve my problem!

Instead of creating and returning a new promise by my self, i now just use .then() that itself does return a promise (since jQuery 1.8). That is all i needed to change to get it to work:

Old:

function test(g) {
  return g.map(e => function(){
    let d = new $.Deferred();
    ajax(e).done(data => d.resolve(data+"2"));
    return d.promise();
  });
}

New:

function test(g) {
      return g.map(e => ajax(e).then(data => data+"2"));
}

The final solution does look like this:

var consoleLine = "<p class=\"console-line\"></p>";

console = {
    log: function (text) {
        $("#console-log").append($(consoleLine).html(text));
    }
};

var list = ["first", "second", "third"];

co(function*(){
  let res = yield list.map(e => ajax(e).then(r => r+"2"));
  res.forEach((a, b, c) => console.log(a));
});

function ajax(data){
  return new Promise(function (resolve, reject) {
       setTimeout(() => resolve(data+"1"), 2000)
  });
}
.console-line
{
    font-family: monospace;
    margin: 2px;
}
<script src="https://raw.githubusercontent.com/tj/co/master/index.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="console-log"></div>

Thank you rodneyrehm for pointing me to the solution.

Spenhouet
  • 6,556
  • 12
  • 51
  • 76
0

I am not fond of using generators with promises. I will re consider this when async await of ES7 comes into life. I believe this job can nicely be done in a functional manner by utilizing a Array.prototype.reduce() if these are dependent in their order in the array and needed to be run one after the other. Otherwise i would recommend a combo of Array.prototype.map() and Promise.all.

Assuming error first callbacks are used.

function promisify(fun){
  return (data) => new Promise((resolve,reject) => fun(data, (err,res) => err ? reject(err) : resolve(res)));
}

function async(data, callback){
  data.val+= " msec"                                  // do something with the data asynchronously
  setTimeout(_ => callback(false,data.val),data.dur); // we are expected to handle returned data in our callback
}

function myCallback(result){     // supposed to work in the asych function but now moved to then stage
  console.log(result);
  return result;
}

var      dataSet = [100, 200, 300],
promisifiedAsych = promisify(async),
 chainedPromises = dataSet.reduce((prom,data) => prom.then(_ => promisifiedAsych({val:data, dur:data*10}))
                                                     .then(myCallback), Promise.resolve());
chainedPromises.then(val => console.log("dataSet completed and returned", val))
Redu
  • 25,060
  • 6
  • 56
  • 76