Realtime database cloud function sends result back before finishing - javascript

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,
};
}
})
);
});
});

Related

How do you do a forEach loop on an array containing data retrieved from firebase database JavaScript

Here is my database Structure:
I am attempting to write a firebase function that goes through every barbershop, and retrieves all their Barbers.
In my code below, I have successfully retrieved all the barbershop names, and stored them in an array, which is logged on the console like so:
However when i attempt to move to the next phase of my function, none of the code in "barberShopArray.forEach((key)" executes, and I don't know why. Even "console.log(key)" doesn't work.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var database = admin.database();
exports.addTimeNod2 = functions.pubsub.schedule('every 24 hours').onRun((context) =>
{
var num = 1;
let barberShopArray = [];
return database.ref('/BarberShops/').once("value").then((snapshot) =>
{
snapshot.forEach((childSnapshot) =>
{
barberShopArray.push(childSnapshot.key);
});
console.log(barberShopArray);
return barberShopArray;
}).then(barberShopArray.forEach((key) =>
{
console.log(key);
database.ref('/BarberShops/' + key + '/Barbers/Barber1/').once("value").then((snapshot)=>
{
if(snapshot.exists())
{
database.ref('metadata/shop' + num +'/').set(key);
num++;
}
return null;
}).catch((error)=>
{
console.log(error);
return error;
});
return null;
}));
});
In my case, I did
const db = firebase.database().ref();
db.child("orders")
.get()
.then((snapshot) => {
if (snapshot.exists()) {
const fetchedOrders = [];
for (let key in snapshot.val()) {
fetchedOrders.push({ ...snapshot.val()[key], id: key });
}
})
For more reference checkout this Link
You have mentioned that the line console.log(barberShopArray) is working perfectly. After the line console.log(barberShopArray) and before the line barberShopArray.forEach((key) you are using a return statement return barberShopArray. So the part of the function which is after that return statement is not getting executed. Please remove that return statement to resolve the issue. Also the then() method after the return barberShopArray statement is not required. So please modify the code as the following and it should work and successfully update the metadata.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var database = admin.database();
exports.addTimeNod2 = functions.pubsub.schedule('every 24 hours').onRun((context) => {
var num = 1;
let barberShopArray = [];
return database.ref('/BarberShops/').once("value").then((snapshot) => {
snapshot.forEach((childSnapshot) => {
barberShopArray.push(childSnapshot.key);
});
console.log(barberShopArray);
barberShopArray.forEach((key) => {
console.log(key);
database.ref('/BarberShops/' + key + '/Barbers/Barber1/').once("value").then((snapshot) => {
if (snapshot.exists()) {
database.ref('metadata/shop' + num + '/').set(key);
num++;
}
}).catch((error) => {
console.log(error);
return error;
});
});
return null;
});
});

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.

How to trigger firebase http function in node.js?

I am trying to trigger an another function in Firebase Cloud function with javascript. But i always getting an error of Can't set headers after they are sent. Please take a look at my code below: ................. ................. ............ ................ ................. ............... ....................... .................. ..............
exports.productIndexShuffleOne = functions.https.onRequest(async (req, res) => {
const interval = req.query.interval;
console.log("interval: "+interval);
const productRef = admin.firestore().collection("Products");
const adminRef = admin.firestore().collection("Admin").doc("totalProd").get();
const dateRef = admin.firestore().collection("Admin").doc("totalProd").collection("indexShuffle").doc("productShuffle").get();
return dateRef.then(documentSnapshot => {
const setDate = documentSnapshot.get('date').seconds;
var nextDay = setDate;
console.log("Date: "+nextDay);
const x = setInterval(function() {
clearInterval(x);
return Promise.all([adminRef]).then(result => {
const totalNum = result[0].data().totalNumber;
console.log("totalNum: "+totalNum);
var numberList = [];
var index = 1;
while(index <= totalNum){
numberList.push(index);
index++;
}
var cidx, ridx, tmp;
cidx = numberList.length;
while (cidx !== 0) {
ridx = Math.floor(Math.random() * cidx);
cidx--;
tmp = numberList[cidx];
numberList[cidx] = numberList[ridx];
numberList[ridx] = tmp;
}
console.log(numberList);
var counter = 0;
return productRef.get().then(snapshot => {
snapshot.forEach(doc => {
const prodID = doc.get('productID');
const index = doc.get('index');
var newIndex = numberList[counter];
counter++;
console.log("oldIndex: "+index);
console.log("newIndex: "+newIndex);
productRef.doc(prodID).update({
index: newIndex
}, {merge: true});
});
return res.redirect('https://us-central1-myfunction-123456.cloudfunctions.net/productIndexShuffleTwo?interval='+interval);
})
.catch(err => {
console.log('Error getting documents', err);
});
});
}, interval);
return res.status(203).send(interval);
}).catch(function(err) {
console.error(err);
});
});
This is because you've sent multiple responses while the rule is that you only allowed sending one response. Please try to look at your code and optimize it in such a way that it contains only one response.
I can see you have multiple responses as below:
1 -> return res.redirect('https://us-central1-myfunction-123456.cloudfunctions.net/productIndexShuffleTwo?interval='+interval);
2 -> return res.status(203).send(interval);
I believe that you can have res.redirect and then res.status.send called one after another. When you writing endpoints there rule of a thumb: always send response and only do that once. Refactor your code so there no way you can make those two calls, but only one of them.

Node.js Promises within promises not waiting for for loop to return data

The return Promise.all([photoArray]) returns an empty array, seemingly not waiting for the callFB to return its promise that then pushes into the array.
I am not sure what I am doing wrong but am relatively new to Promises with for loops and Ifs.
I am not sure exactly if I am using the correct number of Promises but I seem to not be able to get the 3rd tier Promise.all to wait for the for loop to actually finish (in this scenario, the for loop has to look through many item so this is causing an issue where it is not triggering callFeedback for all the items it should before context.done() gets called.
I have tried using Q.all also for the Promise.all([photoArray]) but have been unable to get that working.
module.exports = function (context, myBlob) {
var res = myBlob
var promiseResolved = checkPhoto(res,context);
var promiseResolved2 = checkVideo(res,context);
Promise.all([promiseResolved, promiseResolved2]).then(function(results){
context.log(results[0], results[1]);
// context.done();
});
});
};
};
function checkPhoto(res, context){
return new Promise((resolve, reject) => {
if (res.photos.length > 0) {
var photoArray = [];
for (var j = 0; j < res.photos.length; j++) {
if (res.photos[j].feedbackId !== null){
var feedbackId = res.photos[j].feedbackId;
var callFB = callFeedback(context, feedbackId);
Promise.all([callFB]).then(function(results){
photoArray.push(results[0]);
});
} else {
photoArray.push("Photo " + j + " has no feedback");
}
}
return Promise.all([photoArray]).then(function(results){
context.log("end results: " + results);
resolve(photoArray);
});
} else {
resolve('No photos');
}
})
}
function checkVideo(res, context){
return new Promise((resolve, reject) => {
same as checkPhoto
})
}
function callFeedback(context, feedbackId) {
return new Promise((resolve, reject) => {
var requestUrl = url.parse( URL );
var requestBody = {
"id": feedbackId
};
// send message to httptrigger to message bot
var body = JSON.stringify( requestBody );
const requestOptions = {
standard
};
var request = https.request(requestOptions, function(res) {
var data ="";
res.on('data', function (chunk) {
data += chunk
// context.log('Data: ' + data)
});
res.on('end', function () {
resolve("callFeedback: " + true);
})
}).on('error', function(error) {
});
request.write(body);
request.end();
})
}
The code suffers from promise construction antipattern. If there's already a promise (Promise.all(...)), there is never a need to create a new one.
Wrong behaviour is caused by that Promise.all(...).then(...) promise isn't chained. Errors aren't handled and photoArray.push(results[0]) causes race conditions because it is evaluated later than Promise.all([photoArray])....
In case things should be processed in parallel:
function checkPhoto(res, context){
if (res.photos.length > 0) {
var photoArray = [];
for (var j = 0; j < res.photos.length; j++) {
if (res.photos[j].feedbackId !== null){
var feedbackId = res.photos[j].feedbackId;
var callFB = callFeedback(context, feedbackId);
// likely no need to wait for callFB result
// and no need for Promise.all
photoArray.push(callFB);
} else {
photoArray.push("Photo " + j + " has no feedback");
}
}
return Promise.all(photoArray); // not [photoArray]
} else {
return 'No photos';
};
}
callFB promises don't depend on each other and thus can safely be resolved concurrently. This allows to process requests faster.
Promise.all serves a good purpose only if it's used to resolve promises in parallel, while the original code tried to resolve the results (results[0]).
In case things should be processed in series the function benefits from async..await:
async function checkPhoto(res, context){
if (res.photos.length > 0) {
var photoArray = [];
for (var j = 0; j < res.photos.length; j++) {
if (res.photos[j].feedbackId !== null){
var feedbackId = res.photos[j].feedbackId;
const callFBResult = await callFeedback(context, feedbackId);
// no need for Promise.all
photoArray.push(callFBResult);
} else {
photoArray.push("Photo " + j + " has no feedback");
}
}
return photoArray; // no need for Promise.all, the array contains results
} else {
return 'No photos';
};
}
Add try..catch to taste.

How to maintain Timeout Session when calling Api Call in Loop Contion

i having Api Call which execute in For Loop some of the value which returns 10 sec itself some may take nearly 60 sec i have to maintain proper Timeout and clear session (i.e if results comes at 15 sec means it should goes to next input values and run the code) but currenly its waiting for 45 sec each single record how to optimize it
here my sample code :
if (selectedrows.length >= 1) {
for (var i = 0; i < selectedrows.length; i++) {
var myVar = setTimeout (function (k) {
var ob = { results: "Appending ..." };
child.update(selectedrows[k][4], selectedrows[k][4], ob);
var fullName = selectedrows[k][1] + ' ' + selectedrows[k][2];
math.ResultCall.async(fullName,function (err, res) {
if (err) throw err;
var returnedValue = JSON.parse(res);
console.log(returnedValue);
if(returnedValue.Result == null || returnedValue.Result.FOUND_Result == null)
{
console.log("None found")
}
else{
var obj = { results: “res” };
child.update(selectedrows[k][4], selectedrows[k][4], obj);
}
}
});
}, i * 45000,i);
}
}
Rephrasing your question, you need to return the data when your api gets resolved.
For this please go through https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
JavaScript, by default it work asynchronously because of its event loop.
You have promises and resolve to get notified when your api returns a data
Hope I helped :)
There are several approaches to implement the solution
1. Async-Await: in-case the records-processing order is important
for( let i=0; i<selectedrows.length; i++)
{
let ob = { results: "Appending ..." };
child.update(selectedrows[i][4], selectedrows[i][4], ob);
let fullName = selectedrows[i][1] + ' ' + selectedrows[i][2];
await new Promise((resolve,reject)=>
{
math.ResultCall.async(fullName,(err, res) => {
if (err) reject(err);
let returnedValue = JSON.parse(res);
console.log(returnedValue);
if(returnedValue.Result == null || returnedValue.Result.FOUND_Result == null) {
console.log("None found")
} else {
let obj = { results: “res” };
child.update(selectedrows[i][4], selectedrows[i][4], obj);
}
resolve();
});
}
**don't forget this means the wrapping function should be async as well (which returns a promise that can be resolved if necessary)
2.Promise.All: if the order is not important
let promArray = [];
for( let i=0; i<selectedrows.length; i++)
{
let ob = { results: "Appending ..." };
child.update(selectedrows[i][4], selectedrows[i][4], ob);
let fullName = selectedrows[i][1] + ' ' + selectedrows[i][2];
promArray.push( new Promise((resolve,reject)=>
{
math.ResultCall.async(fullName,(err, res) => {
if (err) reject(err);
let returnedValue = JSON.parse(res);
console.log(returnedValue);
if(returnedValue.Result == null || returnedValue.Result.FOUND_Result == null) {
console.log("None found")
} else {
let obj = { results: “res” };
child.update(selectedrows[i][4], selectedrows[i][4], obj);
}
resolve();
});
);
}
Promise.all(promArray);
** this will also return a Promise that can be resolved if necessary.

Categories