1

I have a list of urls I wish to fetch. All of these urls returns a json object with a property valid. But only one of the fetch promises has the magic valid property to true.

I have tried various combinations of url.forEach(...) and Promises.all([urls]).then(...). Currently my setup is:

const urls = [
    'https://testurl.com', 
    'https://anotherurl.com', 
    'https://athirdurl.com' // This is the valid one
];

export function validate(key) {
    var result;
    urls.forEach(function (url) {
        result = fetch(`${url}/${key}/validate`)
            .then((response) => response.json())
            .then((json) => {
                if (json.license.valid) {
                    return json;
                } else {
                   Promise.reject(json);
                }
            });
    });

    return result;
}

The above is not working because of the async promises. How can I iterate my urls and return when the first valid == true is hit?

janhartmann
  • 14,713
  • 15
  • 82
  • 138
  • So, you are close but are too focused on the fetch/promise. All you need is one additional deferred that the fetch with valid = true will resolve. – Chris Caviness Jan 06 '17 at 14:48
  • Do you still want to fire all requests at the same time? Or in an Async series? – ste2425 Jan 06 '17 at 14:53

4 Answers4

4

Let me throw a nice compact entry into the mix

It uses Promise.all, however every inner Promise will catch any errors and simply resolve to false in such a case, therefore Promise.all will never reject - any fetch that completes, but does not have license.valid will also resolve to false

The array Promise.all resolves is further processed, filtering out the false values, and returning the first (which from the questions description should be the ONLY) valid JSON response

const urls = [
    'https://testurl.com', 
    'https://anotherurl.com', 
    'https://athirdurl.com' // This is the valid one
];

export function validate(key) {
    return Promise.all(urls.map(url => 
        fetch(`${url}/${key}/validate`)
        .then(response => response.json())
        .then(json => json.license && json.license.valid && json)
        .catch(error => false)
    ))
    .then(results => results.filter(result => !!result)[0] || Promise.reject('no matches found'));
}
Jaromanda X
  • 53,868
  • 5
  • 73
  • 87
1

Note that it's impossible for validate to return the result (see here for why). But it can return a promise for the result.

What you want is similar to Promise.race, but not quite the same (Promise.race would reject if one of the fetch promises rejected prior to another one resolving with valid = true). So just create a promise and resolve it when you get the first resolution with valid being true:

export function validate(key) {
    return new Promise((resolve, reject) => {
        let completed = 0;
        const total = urls.length;
        urls.forEach(url => {
            fetch(`${url}/${key}/validate`)
                .then((response) => {
                    const json = response.json();
                    if (json.license.valid) {
                        resolve(json);
                    } else {
                        if (++completed === total) {
                            // None of them had valid = true
                            reject();
                        }
                    }
                })
                .catch(() => {
                    if (++completed === total) {
                        // None of them had valid = true
                        reject();
                    }
                });
        });
    });
}

Note the handling for the failing case.

Note that it's possible to factor out those two completed checks if you like:

export function validate(key) {
    return new Promise((resolve, reject) => {
        let completed = 0;
        const total = urls.length;
        urls.forEach(url => {
            fetch(`${url}/${key}/validate`)
                .then((response) => {
                    const json = response.json();
                    if (json.license.valid) {
                        resolve(json);
                    }
                })
                .catch(() => {
                    // Do nothing, converts to a resolution with `undefined`
                })
                .then(() => {
                    // Because of the above, effectively a "finally" (which we
                    // may get on Promises at some point)
                    if (++completed === total) {
                        // None of them had valid = true.
                        // Note that we come here even if we've already
                        // resolved the promise -- but that's okay(ish), a
                        // promise's resolution can't be changed after it's
                        // settled, so this would be a no-op in that case
                        reject();
                    }
                });
        });
    });
}
Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
0

This can be done using SynJS. Here is a working example:

var SynJS = require('synjs');
var fetchUrl = require('fetch').fetchUrl;

function fetch(context,url) {
    console.log('fetching started:', url);
    var result = {};
    fetchUrl(url, function(error, meta, body){
        result.done = true;
        result.body = body;
        result.finalUrl = meta.finalUrl; 
        console.log('fetching finished:', url);
        SynJS.resume(context);
    } );

    return result;
}

function myFetches(modules, urls) {
    for(var i=0; i<urls.length; i++) {
        var res = modules.fetch(_synjsContext, urls[i]);
        SynJS.wait(res.done);
        if(res.finalUrl.indexOf('github')>=0) {
            console.log('found correct one!', urls[i]);
            break;
        }
    }
};

var modules = {
        SynJS:  SynJS,
        fetch:  fetch,
};

const urls = [
              'http://www.google.com', 
              'http://www.yahoo.com', 
              'http://www.github.com', // This is the valid one
              'http://www.wikipedia.com'
          ];

SynJS.run(myFetches,null,modules,urls,function () {
    console.log('done');
});

It would produce following output:

fetching started: http://www.google.com
fetching finished: http://www.google.com
fetching started: http://www.yahoo.com
fetching finished: http://www.yahoo.com
fetching started: http://www.github.com
fetching finished: http://www.github.com
found correct one! http://www.github.com
done
amaksr
  • 7,555
  • 2
  • 16
  • 17
-1

If you want to avoid the fact of testing each URL, you can use the following code.

const urls = [
    'https://testurl.com', 
    'https://anotherurl.com', 
    'https://athirdurl.com' // This is the valid one
];

export function validate(key) {
 return new Promise((resolve, reject) => {
  function testUrl(url) {
     fetch(`${url}/${key}/validate`)
       .then((response) => response.json())
        .then((json) => {
         if (json.license.valid) {
            resolve(json);
           return;
          }
          if (urlIndex === urls.length) {
            reject("No matches found...");
            return;
          }
          testUrl(urls[urlIndex++]);
        });
    }

  let urlIndex = 0;
    if (!urls.length)
     return reject("No urls to test...");
    testUrl(urls[urlIndex++]);
  });
}
Romain
  • 801
  • 1
  • 6
  • 13