Get result from two promises in Firebase Database - javascript

I have the next function in my WebApp with Firebase:
function loadMonthData(){
let ganancias = 0;
let perdidas = 0;
let thisMonth = new Date();
thisMonth.setHours(0);
thisMonth.setMinutes(0);
thisMonth.setMilliseconds(0);
thisMonth.setDate(1);
fireIngresos.orderByChild('timestamp')
.startAt(thisMonth.getTime())
.once('value')
.then((snapshot)=>{
snapshot.forEach((ingreso)=>{
ganancias += ingreso.val().cash;
});
});
fireGastos.orderByChild('timestamp')
.startAt(thisMonth.getTime())
.once('value')
.then((snapshot)=>{
snapshot.forEach((perdida)=>{
perdidas += perdida.val().cash;
});
});
return ganancias - perdidas;
}
This get the sum of the property cash of all elements in my references fireIngresos and FireGastos (from the beginning of the month), then this return the difference of the two results.
The problem (evidently) is the promises ¿How can I do this correctly?

You can use async/await, return a value from .then()
async function loadMonthData(){
let ganancias = 0;
let perdidas = 0;
let thisMonth = new Date();
thisMonth.setHours(0);
thisMonth.setMinutes(0);
thisMonth.setMilliseconds(0);
thisMonth.setDate(1);
return await fireIngresos.orderByChild('timestamp')
.startAt(thisMonth.getTime())
.once('value')
.then(snapshot => {
snapshot.forEach(ingreso => {
ganancias += ingreso.val().cash;
});
return ganacias
})
- await fireGastos.orderByChild('timestamp')
.startAt(thisMonth.getTime())
.once('value')
.then(snapshot => {
snapshot.forEach(perdida => {
perdidas += perdida.val().cash
});
return peridadas
});
}
loadMonthData().then(result => {// do stuff with result});
function promise(n) {
return new Promise(resolve =>
setTimeout(resolve, Math.floor(Math.random() * 1000), n)
).then(data => data)
}
async function diff() {
return await promise(2 * 4) - await promise(2 * 2);
}
diff().then(res => console.log(res)); // 4

Related

Realtime database cloud function sends result back before finishing

I'm trying to perform some realtime database tasks, which after completion, should send back a result back to the client so the client knows when the tasks have finished.
exports.createGame = functions.https.onCall((data, context) => {
const gameChosen = data.gameChosen;
const adminName = data.adminName;
const numberOfRounds = data.numberOfRounds;
let gameID = Math.floor(100000 + Math.random() * 900000).toString();
var numberOfQuestions = 0;
var questionsPicked = [];
getGameQuestions(gameChosen, numberOfRounds).then((questionsPickedArray) => {
db.ref(`live-games/${gameChosen}/${gameID}`).set(
{
playerAdmin: adminName,
game: gameChosen,
players: [adminName],
questions: questionsPickedArray,
datetimeCreated: Date.now(),
},
(error) => {
if (error) {
console.log("Data could not be saved." + error);
} else {
console.log("Data saved successfully.");
return {
gameChosen: gameChosen,
gameAdmin: adminName,
questions: questionsPicked,
gameID: gameID,
};
}
}
);
});
});
function getGameQuestions(gameChosen, numberOfRounds) {
var questionsPicked = [];
var numberOfQuestions = 0;
return new Promise(function (resolve, reject) {
db.ref(`games/${gameChosen}`).once("value", function (snapshot) {
val = snapshot.val();
numberOfQuestions = val.questionsAvailable;
for (var i = 0; i < numberOfRounds; i++) {
let questionNumber = Math.floor(Math.random() * (numberOfQuestions - 0 + 1) + 0);
if (!questionsPicked.includes(questionNumber)) {
questionsPicked.push(questionNumber);
}
}
resolve(questionsPicked);
});
});
}
I've tried to create a promise due to the fact some realtime database tasks do not return a promise - wasn't sure which one.
Based on the logs, the function completed with a status code of 200 and then a couple seconds after, the realtime database gets updated with the values. The database should be updated, the result sent back to the client and then the function should finish. It's currently sending back NULL to the client - presuming the function is sending it back as soon as it runs.
How does one perform realtime database tasks one after another efficiently?
The following modifications should do the trick:
exports.createGame = functions.https.onCall((data, context) => {
const gameChosen = data.gameChosen;
const adminName = data.adminName;
const numberOfRounds = data.numberOfRounds;
let gameID = Math.floor(100000 + Math.random() * 900000).toString();
var numberOfQuestions = 0;
var questionsPicked = [];
return getGameQuestions(gameChosen, numberOfRounds) // We return the Promises chain, see below for more details
.then((questionsPickedArray) => {
return db.ref(`live-games/${gameChosen}/${gameID}`).set(
{
playerAdmin: adminName,
game: gameChosen,
players: [adminName],
questions: questionsPickedArray,
datetimeCreated: Date.now(),
})
})
.then(() => {
return {
gameChosen: gameChosen,
gameAdmin: adminName,
questions: questionsPicked,
gameID: gameID,
};
})
.catch((error) => {
// See https://firebase.google.com/docs/functions/callable#handle_errors
throw new functions.https.HttpsError(...);
});
});
function getGameQuestions(gameChosen, numberOfRounds) {
var questionsPicked = [];
var numberOfQuestions = 0;
return db.ref(`games/${gameChosen}`).once("value") // Here too, we return the Promises chain
.then(snapshot => {
val = snapshot.val();
numberOfQuestions = val.questionsAvailable;
for (var i = 0; i < numberOfRounds; i++) {
let questionNumber = Math.floor(Math.random() * (numberOfQuestions - 0 + 1) + 0);
if (!questionsPicked.includes(questionNumber)) {
questionsPicked.push(questionNumber);
}
}
return questionsPicked;
});
}
So, you need to chain the Promises and return the result to the client as shown above: as explained in the doc "the data returned by the promise is sent back to the client" (which is the case if we return the Promises chain) and the data shall be an object/value that can be JSON encoded (which is the case with the { gameChosen: gameChosen, gameAdmin: adminName, ...} object).
For the getGameQuestions() function, since the once() method returns a Promise, you don't need to encapsulate it into a Promise. Again, just return the promises chain composed by once().then(...).
You have to return that Promise as mentioned here.
To return data after an asynchronous operation, return a promise.
exports.createGame = functions.https.onCall((data, context) => {
const gameChosen = data.gameChosen;
const adminName = data.adminName;
const numberOfRounds = data.numberOfRounds;
let gameID = Math.floor(100000 + Math.random() * 900000).toString();
var numberOfQuestions = 0;
var questionsPicked = [];
getGameQuestions(gameChosen, numberOfRounds).then((questionsPickedArray) => {
//Just add a return here
return db.ref(`live-games/${gameChosen}/${gameID}`).set(
{
playerAdmin: adminName,
game: gameChosen,
players: [adminName],
questions: questionsPickedArray,
datetimeCreated: Date.now(),
})
.catch((error) => {
if (error) {
console.log("Data could not be saved." + error);
} else {
console.log("Data saved successfully.");
return {
gameChosen: gameChosen,
gameAdmin: adminName,
questions: questionsPicked,
gameID: gameID,
};
}
})
);
});
});

Break the loop in the map function and move

So basically im working on a cron job in my app that fires every 3 hours and updating users 'score' by calling the RiotApi
basically the function so far
exports.updatePlayersPoints = async () => {
console.log('STARTED UPDATING');
try {
const players = await UserLadder.findAll();
await Promise.all(
players.map(async (player) => {
const p = await RiotAccount.findOne({
where: {
userId: player.userId,
},
include: RiotRegions,
});
const beginTime = new Date(player.dataValues.createdAt);
let data;
try {
const res = await axios.get(
`https://${
p.dataValues.riot_region.dataValues.name
}.api.riotgames.com/lol/match/v4/matchlists/by-account/${
p.dataValues.accountId
}?queue=420&beginTime=${beginTime.getTime()}&api_key=${
process.env.RIOT_KEY
}`
);
data = res.data;
} catch (error) {
if (!error.response.status === 404) {
console.error(error);
}
}
if (!data) {
return;
}
let totalScore = player.dataValues.userPoints;
await Promise.all(
data.matches.map(async (match, i) => {
if (i < 15) {
const { data } = await axios.get(
`https://${p.dataValues.riot_region.dataValues.name}.api.riotgames.com/lol/match/v4/matches/${match.gameId}?api_key=${process.env.RIOT_KEY}`
);
const calculateScore = () => {
return new Promise((resolve) => {
const { stats } = _.find(
data.participants,
(o) => o.championId === match.champion
);
const killsPts = stats.kills * 2;
const deathPts = stats.deaths * -1.5;
const assistsPts = stats.assists;
const wardsPts = stats.wardsPlaced / 4;
const firstBloodPts = stats.firstBloodKill ? 3 : 0;
const firstBloodAssistPts = stats.firstBloodAssist ? 3 : 0;
const firstTowerPts = stats.firstTowerKill ? 2 : 0;
const firstTowerAssistPts = stats.firstTowerAssist ? 2 : 0;
const score =
killsPts +
deathPts +
assistsPts +
wardsPts +
firstBloodPts +
firstBloodAssistPts +
firstTowerPts +
firstTowerAssistPts;
totalScore += score;
resolve();
});
};
await calculateScore();
}
})
);
const user = await UserLadder.findOne({
where: {
userId: player.userId,
},
});
user.userPoints = parseFloat(totalScore);
user.lastGameId = data.matches[0].gameId;
await user.save();
})
);
console.log('FINISHED UPDATING');
} catch (error) {
console.error(error);
}
};
Basically it just looks up the table userladder to find the players that are signed to the ladder and for each one of these players it fires a map function that makes a request to the riotapi to get the match history of this player and then later make an inside map function to map each one of these matches.
but basically I updated it to now keep track of the game id of the last call before 3 hours so it doesn't have to make request that was already done.
user.lastGameId = data.matches[0].gameId;
but now in my second map function that maps the matches I wasn't it so that if the last game from my database matches the game id that currently being mapped I want to stop the map function and not continue this record or the ones after because it also means they all have been already counted.
but I can not seem to find a way to do it.
i tried using break; but it didn't work
any ideas?
using for loop
I tried a small test with for loop so I tried
for (let i = 0; i < 15; i++) {
await new Promise(async (resolve, reject) => {
const match = data.matches[i];
console.log(match);
resolve();
if (i === 1) {
break;
}
});
}
but I still go the same error
SyntaxError: Illegal break statement
Instead of trying to "break" a map, you should filter the matches that you want to process before you execute the map.
Something like this:
await Promise.all(
const filteredMatches = data.matches.filter(match => match.gameId > previousId);
filteredMatches.map(async (match, i) => { ...
More on filter() in javascript.
Edit: If generated id's are random and are not ordered, you can store all previous id's in a Set, and then just ask if it has been previously added
await Promise.all(
const filteredMatches = data.matches.filter(match => mySet.has(match.gameId));
filteredMatches.map(async (match, i) => { ...
More on Set in javascript.

Javascript axios post after a while loop async / promise

I'm new with async/await and I need a litle help.
What I would like to achive is to send and axios post request after a while loop finished.
How can I put the while loop in an async function and await for it?
This is the current code:
showResults: function () {
let vm = this;
let apiUrl = '/api/test';
let randomCallCount = Math.floor(Math.random() * (80 - 50 + 1) + 50);
let start = 1;
while (start <= randomCallCount) {
let randomChars = [...Array(40)].map(i => (~~(Math.random() * 36)).toString(36)).join('');
fetch('https://' + randomChars + '.ipleak.net/json/?query_type=mydns')
.then((resp) => resp.json())
.then(function (data) {
vm.dnsResult.push(data);
});
start++;
}
axios.post(apiUrl, {lat: vm.geoLat, lon: vm.geoLon, dns: vm.dnsResult})...
I thought maybe something like this, but this one is not working:
fetchDNSData: async function () {
let vm = this;
let promise = new Promise((resolve, reject) => {
let randomCallCount = Math.floor(Math.random() * (80 - 50 + 1) + 50);
let start = 1;
while (start <= randomCallCount) {
let randomChars = [...Array(40)].map(i => (~~(Math.random() * 36)).toString(36)).join('');
fetch('https://' + randomChars + '.ipleak.net/json/?query_type=mydns')
.then((resp) => resp.json())
.then(function (data) {
vm.dnsResult.push(data);
});
start++;
}
});
let result = await promise; // wait until the promise resolves (*)
return result;
},
showResults: function () {
let vm = this;
let apiUrl = '/api/test';
vm.fetchDNSData().then(
response => {
axios.post(apiUrl, {lat: vm.geoLat, lon: vm.geoLon, dns: vm.dnsResult})...
Any suggestion what can show me the right direction? :) Thanks a lot
If you are going to use async/await, you should not use then. Use await instead of then.
The below example should be what you need.
showResults: async function () {
let vm = this;
let apiUrl = '/api/test';
let randomCallCount = Math.floor(Math.random() * (80 - 50 + 1) + 50);
let start = 1;
while (start <= randomCallCount) {
let randomChars = [...Array(40)].map(i => (~~(Math.random() * 36)).toString(36)).join('');
const response = await fetch('https://' + randomChars + '.ipleak.net/json/?query_type=mydns');
const data = await response.json();
vm.dnsResult.push(data);
start++;
}
axios.post(apiUrl, {lat: vm.geoLat, lon: vm.geoLon, dns: vm.dnsResult})...

How can I use loops/recursion with promise-chains?

Imagine for example that you want to store paginated data from an API to a database.
let db;
let pageitems = 35
var offset = 0;
dbConnect //establish connection to database
.then( fetch(apiLink+?offset=2)
.then( res => res.json())
.then( res => {
var total = res.count
return collection.insertMany(res.data, {ordered: false})
// If offset is less than total, I want to increase offset and go back to the fetch-event.
.catch( err => {
if(err.code !== 11000){log(err)}
else{log({completed: err.result.nInserted, duplicates:
err.result.result.writeErrors.length});}
})
.then(() => {
connection.close();
})
You could just use a regular loop:
(async function() {
const conn = await dbConnect;
for(let offset = 0; true; offset++) {
const { data, count } = await (await fetch(`api?page=${offset}`)).json();
// Exit if the page is empty
if(count === 0) break;
await collection.insertMany(data, { ordered: false });
}
})();
To speed that up you could execute multiple requests in parallel:
const chunkSize = 10; // 10 in parallel
for(let offset = 0; offset < chunkSize; offset++) {
(async function() {
const conn = await dbConnect;
for(let offset2 = 0; true; offset2 += chunkSize) {
const { data, count } = await (await fetch(`api?page=${offset + offset2}`)).json();
// Exit if the page is empty
if(count === 0) break;
await collection.insertMany(data, { ordered: false });
}
})();
}
Basically, you will want to wrap your fetch and insert into a function that you will call many times. See the below as an example to illustrate my point...
let db;
let pageitems = 35
var offset = 0;
var db = dbConnect() //establish connection to database
function fetch_and_insert(offset) {
db
.then(fetch(apiLink + "?" + offset))
.then(res => res.json())
.then(res => {
var total = res.count
collection.insertMany(res.data, { ordered: false })
.catch(err => {
if (err.code !== 11000) { log(err) }
else {
log({
completed: err.result.nInserted, duplicates: err.result.result.writeErrors.length
});
}
})
if (offset < total) return fetch_and_insert(offset + pageitems)
return null;
})
}
fetch_and_insert(offset)
.then(() => {
connection.close();
})

Javascript: Add timeout after every request in Promise.all Map function

For the following function, I have to add a timeout after every GET request in array ajaxUrls. All the XHR GET request are in array ajaxUrls.
function getAllSearchResultProfiles(searchAjaxUrl) {
var ajaxUrls = [];
for (var i = 0; i < numResults; i += resultsPerPage) {
ajaxUrls.push(searchAjaxUrl + "&start=" + i);
}
return Promise.all(ajaxUrls.map(getSearchResultsForOnePage))
.then(function(responses) {
return responses.map(function(response) {
if (response.meta.total === 0) {
return [];
}
return response.result.searchResults.map(function(searchResult) {
return (searchResult);
});
});
})
.then(function(searchProfiles) {
return [].concat.apply([], searchProfiles);
})
.catch(function(responses) {
console.error('error ', responses);
});
}
function getSearchResultsForOnePage(url) {
return fetch(url, {
credentials: 'include'
})
.then(function(response) {
return response.json();
});
}
I want a certain timeout or delay after every GET request. I am facing difficulty in where exactly to add the timeout.
If you want to make requests in serial, you shouldn't use Promise.all, which initializes everything in parallel - better to use a reduce that awaits the previous iteration's resolution and awaits a promise-timeout. For example:
async function getAllSearchResultProfiles(searchAjaxUrl) {
const ajaxUrls = [];
for (let i = 0; i < numResults; i += resultsPerPage) {
ajaxUrls.push(searchAjaxUrl + "&start=" + i);
}
const responses = await ajaxUrls.reduce(async (lastPromise, url) => {
const accum = await lastPromise;
await new Promise(resolve => setTimeout(resolve, 1000));
const response = await getSearchResultsForOnePage(url);
return [...accum, response];
}, Promise.resolve([]));
// do stuff with responses
const searchProfiles = responses.map(response => (
response.meta.total === 0
? []
: response.result.searchResults
));
return [].concat(...searchProfiles);
}
Note that only asynchronous operations should be passed from one .then to another; synchronous code should not be chained with .then, just use variables and write the code out as normal.
I find a simple for loop in an async function to be the most readable, even if not necessarily the most succinct for things like this. As long as the function is an async function you can also create a nice pause() function that makes the code very easy to understand when you come back later.
I've simplified a bit, but this should give you a good idea:
function pause(time) {
// handy pause function to await
return new Promise(resolve => setTimeout(resolve, time))
}
async function getAllSearchResultProfiles(searchAjaxUrl) {
var ajaxUrls = [];
for (var i = 0; i < 5; i++) {
ajaxUrls.push(searchAjaxUrl + "&start=" + i);
}
let responses = []
for (url of ajaxUrls) {
// just loop though and await
console.log("sending request")
let response = await getSearchResultsForOnePage(url)
console.log("recieved: ", response)
responses.push(response)
await pause(1000) // wait one second
}
//responses.map() and other manilpulations etc...
return responses
}
function getSearchResultsForOnePage(url) {
//fake fetch
return Promise.resolve(url)
}
getAllSearchResultProfiles("Test")
.then(console.log)
If you want to add a delay in every request then add a setTimout() in your function which fetches data from api
function getSearchResultsForOnePage(url) {
return new Promise((resolve, reject) => {
fetch(url, {
credentials: 'include'
})
.then(response => reresponse.json())
.then(data => {
let timeout = 1000;
setTimeout(() => resolve(data), timeout);
});
}

Categories