Execute two observables at the same time without converting them to promises - javascript

I want to call a number of API endpoints at once (asynchronously). I get an Observable from a single API call, and I want to "await" them all together, and get the results from each call to handle as a collection, preferably with an option to handle the exceptions (if any) from each one. Much like asyncio.gather() but for Angular. I don't want to convert the observables to promises, and I don't want to use deprecated methods such forkJoin() or the async pipe. Is there any other solution?

forkJoin is actually not deprecated but some overrides of it are.
An array of Observables or a dictionary are valid inputs to forkJoin.
This shouldn't throw any warnings about deprecation.
const sources$: Observable<any>[] = [obs1$, obs2$, obs3$]
forkJoin(sources$).subscribe()

So instead of 'converting' the observables to promises (I assume you're referring to toPromise()), you just wrap the subscriptions in promises. Then you use Promise.all() to await them concurrently.
constructor(private http: HttpClient) {}
async waitForResponses() {
const obs1 = this.http.get('api1');
const obs2 = this.http.get('api2');
const obs3 = this.http.get('api3');
const promise1 = new Promise((resolve) =>
obs1.subscribe((result) => resolve(result))
);
const promise2 = new Promise((resolve) =>
obs2.subscribe((result) => resolve(result))
);
const promise3 = new Promise((resolve) =>
obs3.subscribe((result) => resolve(result))
);
const [result1, result2, result3] = await Promise.all<any>([promise1, promise2, promise3]);
console.log(result1);
console.log(result2);
console.log(result3);
}
This awaits all api calls concurrently, but you could await them in any order you like.
Promise.all() returns an array of the results, at the same index as their respective promise. I'm pretty sure that's exactly what you're looking for.
Error handling:
const promise1 = new Promise((resolve, reject) =>
obs1.subscribe({
next: (result) => resolve(result),
error: (err) => reject(err),
})
).catch((err) => console.log(err));

Related

How to handle multiple mutations in parallel with react-query

I have a custom useMutation hook:
const {
status: updateScheduleStatus,
reset: updateScheduleReset,
mutateAsync: updateSchedule,
} = useUpdateSchedule(queryClient, jobId as string);
Which I understand sets up the mutation but how would I use this if I wanted to do multiple parallel mutations?
I have tried to implement the following but the mutations execute prior to getting to the Promise.all(mutations line.
let mutations: Array<any> = [];
schedulesForDeletion.forEach(async (schedule) => {
const resp = await monitoringService?.getSchedule(
schedule.schedule_id,
);
mutations.push(
updateSchedule({
monitoringService: monitoringService as MonitoringServiceClient,
schedule,
etag: resp?.type === "data" ? resp.headers.etag : "",
}),
);
});
console.dir(mutations);
await Promise.all(mutations);
I would have through that as mutateAsync returns a Promise that they would not fire in sequence but seems that they do.
Is there a way to handle this in react-query or am I better off just performing this with axios? It would be useful to do in react-query as I need to invalidate some queries when the mutations are successful.
running multiple mutations in parallel does work with mutateAsync:
const { mutateAsync } = useMutation(num => Promise.resolve(num + 1))
const promise1 = mutateAsync(1)
const promise2 = mutateAsync(2)
await Promise.all([promise1, promise2])
I'm guessing in your example you push a Promise to the array, then you continue your loop and await monitoringService?.getSchedule. Only after that returns, you fire off the second mutation.
So in that sense, it seems that this is what's "blocking" your execution. If you push the original Promise coming from getSchedule, it should work:
schedulesForDeletion.forEach((schedule) => {
mutations.push(
monitoringService?.getSchedule(
schedule.schedule_id,
).then(resp => updateSchedule({...})
)
)
})

Learning Promises, Async/Await to control execution order

I have been studying promises, await and async functions. While I was just in the stage of learning promises, I realized that the following: When I would send out two requests, there was no guarantee that they would come in the order that they are written in the code. Of course, with routing and packets of a network. When I ran the code below, the requests would resolve in no specific order.
const getCountry = async country => {
await fetch(`https://restcountries.com/v2/name/${country}`)
.then(res => res.json())
.then(data => {
console.log(data[0]);
})
.catch(err => err.message);
};
getCountry('portugal');
getCountry('ecuador');
At this point, I hadn't learned about async and await. So, the following code works the exact way I want it. Each request, waits until the other one is done.
Is this the most simple way to do it? Are there any redundancies that I could remove? I don't need a ton of alternate examples; unless I am doing something wrong.
await fetch(`https://restcountries.com/v2/name/${country}`)
.then(res => res.json())
.then(data => {
console.log(data[0]);
})
.catch(err => err.message);
};
const getCountryData = async function () {
await getCountry('portugal');
await getCountry('ecuador');
};
getCountryData();
Thanks in advance,
Yes, that's the correct way to do so. Do realize though that you're blocking each request so they run one at a time, causing inefficiency. As I mentioned, the beauty of JavaScript is its asynchronism, so take advantage of it. You can run all the requests almost concurrently, causing your requests to speed up drastically. Take this example:
// get results...
const getCountry = async country => {
const res = await fetch(`https://restcountries.com/v2/name/${country}`);
const json = res.json();
return json;
};
const getCountryData = async countries => {
const proms = countries.map(getCountry); // create an array of promises
const res = await Promise.all(proms); // wait for all promises to complete
// get the first value from the returned array
return res.map(r => r[0]);
};
// demo:
getCountryData(['portugal', 'ecuador']).then(console.log);
// it orders by the countries you ordered
getCountryData(['ecuador', 'portugal']).then(console.log);
// get lots of countries with speed
getCountryData(['mexico', 'china', 'france', 'germany', 'ecaudor']).then(console.log);
Edit: I just realized that Promise.all auto-orders the promises for you, so no need to add an extra sort function. Here's the sort fn anyways for reference if you take a different appoach:
myArr.sort((a, b) =>
(countries.indexOf(a.name.toLowerCase()) > countries.indexOf(b.name.toLowerCase())) ? 1 :
(countries.indexOf(a.name.toLowerCase()) < countries.indexOf(b.name.toLowerCase()))) ? -1 :
0
);
I tried it the way #deceze recommended and it works fine: I removed all of the .then and replaced them with await. A lot cleaner this way. Now I can use normal try and catch blocks.
// GET COUNTRIES IN ORDER
const getCountry = async country => {
try {
const status = await fetch(`https://restcountries.com/v2/name/${country}`);
const data = await status.json();
renderCountry(data[0]); // Data is here. Now Render HTML
} catch (err) {
console.log(err.name, err.message);
}
};
const getCountryData = async function () {
await getCountry('portugal');
await getCountry('Ecuador');
};
btn.addEventListener('click', function () {
getCountryData();
});
Thank you all.

Catching individual errors when using Fetch API

I'm trying to make multiple fetch calls at once, so far I've used this page to reach a point where I can make multiple calls correctly. However the problem now is if one of those calls returns an error. What I'd like, is that if one URL is good and one is bad, the good URL still returns the JSON object, but the bad URL returns the error. But the catch statement notices one error and stops both calls.
My current code:
let resList = await Promise.all([
fetch(goodURL),
fetch(badURL)
]).then(responses => {
return Promise.all(responses.map(response => {
return response.json();
}))
}).catch(error => {
console.log(error);
});
console.log(resList);
Currently, logging resList returns undefined when I want it to return the JSON object from the good URL
As #jonrsharpe mentioned, .allSettled() is probably your best bet.
You can also basically make your own version with something like this:
const results = await Promise.all(
[a, b, c].map(p =>
p.catch(err => {
/* do something with it if you want */
}
)
);
Basically, instead of having one catch on Promise.all(), you attach a catch to each Promise BEFORE giving it to Promise.all(). If one of them throws an error, it'll trigger its individual catch, which you can do whatever with. As long as you don't return something else, you'll end up with it returning undefined, which you can filter out.
For example, if a and c worked and b errored, your results would look like this:
['a', undefined, 'b']
And it'd be in the same order as you fed the promises in, so you could tell which was the failure.
const a = new Promise((resolve) => setTimeout(() => resolve('a'), 100));
const b = new Promise((_, reject) => setTimeout(() => reject('b'), 200));
const c = new Promise((resolve) => setTimeout(() => resolve('c'), 50));
(async () => {
const results = await Promise.all(
[a, b, c].map(p =>
p.catch(err => console.error('err', err))
)
);
console.log(results);
})();

Why can't I move "await" to other parts of my code?

Edit2: Solution at the bottom
I am using the chrome-console and I am trying to output fetched data, and I only get the desired output by writing "await" at exactly the right place, even though another solution can do it earlier and I don't know why/how it works.
solution() is the "official" solution from a web-course I am doing. Both functions return the same, currently. In myFunction I tried writing "await" in front of every used function and make every function "async", but I still can't replace the "await" inside log, even though the other solution can.
const urls = ['https://jsonplaceholder.typicode.com/users']
const myFunction = async function() {
// tried await before urls/fetch (+ make it async)
const arrfetched = urls.map( url => fetch(url) );
const [ users ] = arrfetched.map( async fetched => { //tried await in front of arrfetched
return (await fetched).json(); //tried await right after return
});
console.log('users', await users); // but can't get rid of this await
}
const solution = async function() {
const [ users ] = await Promise.all(urls.map(async function(url) {
const response = await fetch(url);
return response.json();
}));
console.log('users', users); // none here, so it can be done
}
solution();
myFunction();
I would think "await" works in a way that makes:
const a = await b;
console.log(a); // this doesn't work
the same as
const a = b;
console.log(await a); // this works
but it doesn't, and I don't understand why not. I feel like Promise.all does something unexpected, as simply writing "await" in the declaration can't do the same, only after the declaration.
Edit1: this does not work
const myFunction = async function() {
const arrfetched = await urls.map( async url => await fetch(url) );
const [ users ] = await arrfetched.map( async fetched => {
return await (await fetched).json();
});
console.log('users', users);
}
Edit2: Thanks for the help everyone, I tried putting ".toString()" on a lot of variables and switching where I put "await" in the code and where not.
As far as I understand it, if I don't use Promise.all then I need to await every time I want to use (as in the actualy data, not just use) a function or variable that has promises. It is insufficient to only have await where the data is being procensed and not further up.
In the Edit1 above users runs bevore any other await is complete, therefore no matter how many awaits i write in, none are being executed. Copying this code in the (in my case chrome-)console demostrates it nicely:
const urls = [
'https://jsonplaceholder.typicode.com/users',
]
const myFunction = async function() {
const arrfetched = urls.map( async url => fetch(url) );
const [ users ] = arrfetched.map( async fetched => {
console.log('fetched', fetched);
console.log('fetched wait', await fetched);
return (await fetched).json();
});
console.log('users', users);
console.log('users wait', await users);
}
myFunction();
// Output in the order below:
// fetched()
// users()
// fetched wait()
// users wait()
TL; DR: Promise.all is important there, but it's nothing magical. It just converts an array of Promises into a Promise that resolves with an array.
Let's break down myFunction:
const arrfetched = urls.map( url => fetch(url) );
This returns an array of Promises, all good so far.
const [ users] = arrfetched.map( async fetched => {
return (await fetched).json();
});
You're destructuring an array to get the first member, so it's the same as this:
const arr = arrfetched.map( async fetched => {
return (await fetched).json();
});
const users = arr[0];
Here we are transforming an array of promises into another array of promises. Notice that calling map with an async function will always result in an array of Promises.
You then move the first member of that array into users, so users now actually contains a single Promise. You then await it before printing it:
console.log('users', await users);
In contrast, the other snippet does something slightly different here:
const [ users ] = await Promise.all(urls.map(async function(url) {
const response = await fetch(url);
return response.json();
}));
Once again, let's separate the destructuring:
const arr = await Promise.all(urls.map(async function(url) {
const response = await fetch(url);
return response.json();
}));
const users = arr[0];
Promise.all transforms the array of Promises into a single Promise that results in an array. This means that, after await Promise.all, everything in arr has been awaited (you can sort of imagine await Promise.all like a loop that awaits everything in the array). This means that arr is just a normal array (not an array of Promises) and thus users is already awaited, or rather, it was never a Promise in the first place, and thus you don't need to await it.
Maybe the easiest way to explain this is to break down what each step achieves:
const urls = ['https://jsonplaceholder.typicode.com/users']
async function myFunction() {
// You can definitely use `map` to `fetch` the urls
// but remember that `fetch` is a method that returns a promise
// so you'll just be left with an array filled with promises that
// are waiting to be resolved.
const arrfetched = urls.map(url => fetch(url));
// `Promise.all` is the most convenient way to wait til everything's resolved
// and it _also_ returns a promise. We can use `await` to wait for that
// to complete.
const responses = await Promise.all(arrfetched);
// We now have an array of resolved promises, and we can, again, use `map`
// to iterate over them to return JSON. `json()` _also_ returns a promise
// so again you'll be left with an array of unresolved promises...
const userData = responses.map(fetched => fetched.json());
//...so we wait for those too, and destructure out the first array element
const [users] = await Promise.all(userData);
//... et voila!
console.log(users);
}
myFunction();
Await can only be used in an async function. Await is a reserved key. You can't wait for something if it isn't async. That's why it works in a console.log but not in the global scope.

Bluebird Promise Order issue

I am watching videos to learn MongoDB Express.js VueJS Node.js (MEVN) stack.
And I want to create a seed directory and also use promise functions
// const delay = require('delay')
const Promise = require('bluebird')
const songs = require('./songs.json')
const users = require('./users.json')
const bookmarks = require('./bookmarks.json')
const historys = require('./history.json')
sequelize.sync({ force: true })
.then( async function () {
await Promise.all(
users.map( user => {
User.create(user)
})
)
await Promise.all(
songs.map( song => {
Song.create(song)
})
)
//I have to add this line
// ---> await delay(1000)
await Promise.all(
bookmarks.map( bookmark => {
Bookmark.create(bookmark)
})
)
await Promise.all(
historys.map( history => {
History.create(history)
})
)
})
I have four tables with seeds to create, and the last two tables data must be created after the former two tables data. (They are foreign keys)
But every time I run this file, the last two tables data will be created first
The only way I can prevent this is to add delay(1000) between them.
I am wondering if there exists any efficient way to solve this issue~
Thank you.
Race conditions like this one is always caused by that promises weren't properly chained.
A promise should be returned from map callback:
await Promise.all(
users.map( user => User.create(user))
);
etc.
Not returning a value from map is virtually always a mistake. It can be prevented by using array-callback-return ESLint rule.
If User.create(user), etc. were Bluebird promises with default configuration, not chaining them would also result in this warning.
My assumption why your code might fail:
You're not returning the Promises that I guess /(User|Song|Bookmark|History).create/g return to the Promise.all() function, since your map callback is not returning anything.
If you're using Arrow functions with brackets, then you need to explicitly specify the return value (using the familar return keyword).
Otherwise you can just omit the curly brackets.
My suggestion is, that you refactor you're code by utilizing Promise .then()-Chaining.
For you're example, I would suggest something like this:
const Promise = require('bluebird')
const songs = require('./songs.json')
const users = require('./users.json')
const bookmarks = require('./bookmarks.json')
const histories = require('./history.json')
sequelize.sync({
force: true
}).then(() =>
Promise.all(
users.map(user =>
User.create(user)
)
).then(() =>
Promise.all(
songs.map(song =>
Song.create(song)
)
)
).then(() =>
Promise.all(
bookmarks.map(bookmark =>
Bookmark.create(bookmark)
)
)
).then(() =>
Promise.all(
histories.map(history =>
History.create(history)
)
)
)
);

Categories