Cloud Functions for Firebase - Looping with Promises - javascript

I'm in the process of trying to create a trigger that would add up test scores and then calculate the students placement based on previous test results.
I am attempting to utilize Promises within a FOR loop as seen below:
exports.boxScoresUpdate = functions.database.ref('/Tests/{id}/TestScores').onWrite(event => {
let testScr = 0;
for (let i = 1; i <= section; i++) {
//
testScr += parseInt(nValue[i]);
var index;
admin.database().ref('TestScores').child(data.key).child('Summative').child(i).once("value").then(x => {
xIndex = x.val();
admin.database().ref('TestScores').child(data.key).child('Formative').child(i).once("value")
}).then(y => {
yIndex = y.val();
admin.database().ref('StudentPlacement').child(data.key).child(xIndex + ":" + yIndex).once("value", snapshot => {
// SnapShot
console.log("Student Placement is: ", snapshot.val());
});
}).catch(reason => {
// Handle Error
console.log(reason);
});
}
}
Which I was told would not work as seen in this post.
"Once a promise is resolved or rejected, it forever retains that state and can't be used again. To repeat the work, I think you'd have to construct another chain of new promises representing the second iteration of work."
I have been attempting to restructure my trigger but I can not figure it out, how would I construct the new chain of promises to achieve my desired result?! Has anyone ever encountered and overcome this issue?
The behavior I am hoping to achieve is make the trigger iterate for four (4) iterations section is equal to 4.
I needed to utilize promises else the iteration would not complete correctly, specifically testScr += parseInt(nValue[i]); and the lookup for Summative and Formative.
But as stated, using Promises is working perfectly except it only iterates for the first instance, and not for when the i = 2 or 3 or 4

This approach is not that clean but might help you.
exports.boxScoresUpdate = functions.database.ref('/Tests/{id}/TestScores').onWrite(event => {
let testScr = 0;
for (let i = 1; i <= section; i++) {
//
testScr += parseInt(nValue[i]);
var index;
admin.database().ref('TestScores').child(data.key).child('Summative').child(i).once("value").then(x => {
xIndex = x.val();
return { xIndex, index: i };
}).then(({ xIndex, index}) => {
admin.database().ref('TestScores').child(data.key).child('Formative').child(index).once("value").then(y => {
yIndex = y.val();
return { yIndex, xIndex };
}).then(({ yIndex, xIndex}) => {
admin.database().ref('StudentPlacement').child(data.key).child(xIndex + ":" + yIndex).once("value", snapshot => {
console.log("Student Placement is: ", snapshot.val());
});
});
}).catch(reason => {
console.log(reason);
});
}
});

Related

Values of a promise in JS within for loop

I am lost in the promised land and could really use some guidance. I have exhausted searching numerous SO questions (2-3 hours of reading solutions + docs) related to this seemingly common issue and feel I just am not getting it.
Overview
Below I have code that takes in an Object type (resources), grabs a few values from this Object and then calculates distance and duration from the GoogleMaps Distance Matrix. The results of the function googleRequest() are a promise containing two values (distance and duration).
I would like to get these two values back within the for loop, execute pushToRows(), and then return an array called final_rows.
Problem
final_rows shows UNDEFINED for the duration and distance keys within each row. I speculate this is occurring because I am attempting to access the values in dist_dur inappropriately. I would appreciate any help on resolving this issue. Thanks.
Code
final_rows = []
function getDistTime(resources){
for (var i = 0; i < resources.data.length; i++) {
var origin1 = $("#citystate").val();
var destinationA = resources.data[i]['DEMOBILIZATION CITY'] + ',' + resources.data[i]['DEMOBILIZATION STATE'];
var dist_time_data = googleRequest(origin1, destinationA).then((values) => {
return values
})
pushToRows(resources.data[i], dist_time_data)
}
// console.log(final_rows)
}
function pushToRows(resources, dist_dur){
resources["DISTANCE_MI"] = dist_dur[0];
resources["ACTUAL_DUR_HR"] = dist_dur[1];
resources["FINANCE_DUR_HR"] = (dist_dur[0] / 45.0).toFixed(2)
final_rows.push(resources)
}
So, what you would need to do is just store promises in an array in the for loop and then wait for these promises to resolve using Promise.all but this would parallelize your requests to google distance api.
function getDistTime(resources){
const promiseArr = [];
for (var i = 0; i < resources.data.length; i++) {
var origin1 = $("#citystate").val();
var destinationA = resources.data[i]['DEMOBILIZATION CITY'] + ',' + resources.data[i]['DEMOBILIZATION STATE'];
promiseArr.push(googleRequest(origin1, destinationA));
}
// Not sure how would you use the data pushed in rows but since you are not waiting for promises to be resolved, data would be updated later on
return Promise.all(promiseArr)
.then((resultsArr) => {
resultsArr.forEach((result, i) => pushToRows(resources.data[i], result));
})
}
function pushToRows(resources, dist_dur){
resources["DISTANCE_MI"] = dist_dur[0];
resources["ACTUAL_DUR_HR"] = dist_dur[1];
resources["FINANCE_DUR_HR"] = (dist_dur[0] / 45.0).toFixed(2)
final_rows.push(resources)
}
I would recommend to use async-await which are syntactic sugar to promises but make your code easy to understand and remove the complications that come with promise chaining.
If you move your pushToRows() inside where you return values, you will have access to that data.
googleRequest(origin1, destinationA).then((values) => {
pushToRows(resources.data[i], values);
});
Until that promise resolves, dist_time_data would be undefined
You could also convert to Promise.all() which takes an array of promises and resolves when all of the promises are complete:
function getDistTime(resources){
const promises = [];
for (var i = 0; i < resources.data.length; i++) {
var origin1 = $("#citystate").val();
var destinationA = resources.data[i]['DEMOBILIZATION CITY'] + ',' + resources.data[i]['DEMOBILIZATION STATE'];
promises.push(googleRequest(origin1, destinationA));
}
return Promise.all(promises).then((results) => {
return results.map((result, i) => {
return {
...resources.data[i],
DISTANCE_MI: result[0],
ACTUAL_DUR_HR: result[1],
FINANCE_DUR_HR: (result[0] / 45.0).toFixed(2)
};
});
});
}
getDistTime(resources).then(result => {
//result is now "final_rows"
});

Promise.map: Maximum call stack size exceeded

Im using bluebirds Promise.map() method to run 100,000 firebase queries as shown below and the function takes about 10 seconds to run.. If I set the concurrency higher than 1000 then I receive the error
Maximum call stack size exceeded
Any ideas on how to fix this and also how to speed this up. It seems to me that perhaps Promise.map() may not be the right function to use or maybe I am mismanaging the memory some how. Any ideas thank you.
exports.postMadeByFriend = functions.https.onCall(async (data, context) => {
const mainUserID = "hJwyTHpoxuMmcJvyR6ULbiVkqzH3";
const follwerID = "Rr3ePJc41CTytOB18puGl4LRN1R2"
const otherUserID = "q2f7RFwZFoMRjsvxx8k5ryNY3Pk2"
var refs = [];
for (var x = 0; x < 100000; x += 1) {
if (x === 999) {
const ref = admin.database().ref(`Followers`).child(mainUserID).child(follwerID)
refs.push(ref);
continue;
}
const ref = admin.database().ref(`Followers`).child(mainUserID).child(otherUserID);
refs.push(ref);
}
await Promise.map(refs, (ref) => {
return ref.once('value')
}, {
concurrency: 10000
}).then((val) => {
console.log("Something happened: " + JSON.stringify(val));
return val;
}).catch((error) => {
console.log("an error occured: " + error);
return error;
})
Edits
const runtimeOpts = {
timeoutSeconds: 300,
memory: '2GB'
}
exports.postMadeByFriend = functions.runWith(runtimeOpts).https.onCall(async (data, context) => {
const mainUserID = "hJwyTHpoxuMmcJvyR6ULbiVkqzH3";
const follwerID = "Rr3ePJc41CTytOB18puGl4LRN1R2"
const otherUserID = "q2f7RFwZFoMRjsvxx8k5ryNY3Pk2"
var refs = [];
for (var x = 0; x < 100000; x += 1) {
if (x === 999) {
const ref = admin.database().ref(`Followers`).child(mainUserID).child(follwerID)
refs.push(ref);
continue;
}
const ref = admin.database().ref(`Followers`).child(mainUserID).child(otherUserID);
refs.push(ref);
}
await Promise.map(refs, (ref) => {
return ref.once('value')
}, {
concurrency: 10000
}).then((val) => {
console.log("Something happened: " + JSON.stringify(val));
return val;
}).catch((error) => {
console.log("an error occured: " + error);
return error;
})
Update:
If the goal is to have a number of friends posts the better way to do it would be to have a cloud function that increments a counter on every new post saved to the DB. That way you can get the number of posts with no calculation needed.
Here is the similar answer, and a code sample with the like counter
Original answer:
You could try to increase the memory allocated to your Cloud Funciton:
In the Google Cloud Platform Console, select Cloud Functions from the left menu.
Select a function by clicking on its name in the functions list.
Click the Edit icon in the top menu.
Select a memory allocation from the drop-down menu labeled Memory allocated.
Click Save to update the function.
As described in the Manage functions deployment page

Promise.all() not always resolved - edited to add the code for both functions

I'm using the following code to update the state but the Promise.all() array isn't always resolved before the final function is called. Sometimes, the entire array is populated but it usually isn't.
I'm logging the Promise.all() array to the console just so I can see what's happening. What is really peculiar is the state is always updated properly but calling the individual elements in the array often returns "undefined". I need to use the individual elements to create a new array for displaying later which is why I'm accessing them.
Please help me either figure out how to access the array when it's finished updating or a better way to process the entire thing.
class X {
componentDidMount() {
const numTiles = 3;
for (let i = 0; i < numTiles; i++) {
Promise.all([
this.fetchSong(), // returns JSON from SQLite DB
this.fetchArtists(), // returns JSON from SQLite DB
])
.then(values => {
this.testing(values);
});
}
}
testing(arr) {
console.log("arr: ", arr);
console.log("arr[0]: ", arr[0]);
console.log("arr[0].id: ", arr[0].id);
console.log("arr[0].name: ", arr[0].name);
console.log("arr[0].artist: ", arr[0].artist);
console.log("arr[1]: ", arr[1]);
console.log("arr[1][0]: ", arr[1][0]);
console.log("arr[1][1]: ", arr[1][1]);
console.log("arr[1][0].artist: ", arr[1][0].artist);
console.log("arr[1][1].artist: ", arr[1][1].artist);
}
}
Edit: code for fetchSong() and fetchArtists() added.
fetchSong() {
let id = Math.floor(Math.random() * 2000) + 1; // get random number for id
return new Promise((resolve, reject) => {
Bingo.getSong(id).then(song => {
resolve(song);
});
});
}
fetchArtists() {
return new Promise((resolve, reject) => {
let arr = [];
for (let j = 0; j < 2; j++) {
let id = Math.floor(Math.random() * 10) + 1;
Bingo.getArtist(id).then(artist => {
arr.push(artist);
});
resolve(arr);
};
});
}
The console screenshot below shows the Promise.all() array has been populated but the array elements are still missing.
The problem is in the implementation of fetchArtists, which resolved before any of the single-artist-fetching promises were resolved.
I've also simplified fetchSong, but the gist of fetchArtists now is you store the promises that will resolve to single artists, then wait for all of them to resolve. There's no reason to add a .then() there, since as you know, Promise.all() will resolve with an array of resolved values anyhow.
fetchSong() {
const id = Math.floor(Math.random() * 2000) + 1;
return Bingo.getSong(id);
}
fetchArtists() {
const fetchPromises = [];
for (let j = 0; j < 2; j++) {
const id = Math.floor(Math.random() * 10) + 1;
fetchPromises.push(Bingo.getArtist(id));
}
return Promise.all(fetchPromises);
}

Issue structuring a promise in a loop

I am struggling with getting this resolved, as I am new to Promises.
I need to first read both the Summative and Formative from Firebase before I can determine the StudentPlacement
The way the code below, provides null as the StudentPlacement snapshot.val(), as it is not waiting for the x and y values.
exports.boxScoresUpdate = functions.database.ref('/Tests/{id}/TestScores').onWrite(event => {
let testScr = 0;
for (let i = 1; i <= section; i++) {
//
testScr += parseInt(nValue[i]);
var xIndex = 0;
var yIndex = 0;
admin.database().ref('TestScores').child(data.key).child('Summative').child(i).once("value").then(x => {
xIndex = x.val();
});
admin.database().ref('TestScores').child(data.key).child('Formative').child(i).once("value").then(y => {
yIndex = y.val();
});
admin.database().ref('StudentPlacement').child(data.key).child(xIndex + ":" + yIndex).once("value", snapshot => {
// SnapShot
console.log("Student Placement is: ", snapshot.val());
});
}
}
Can anyone please help me structure the trigger!?
You're waiting for both functions to finish before executing the next bit of code. Look into Promise.all.
for (let i = 1; i <= section; i++) {
const xIndexRef = admin.database().ref('TestScores').child(data.key).child('Summative').child(i).once("value");
const yIndexRef = admin.database().ref('TestScores').child(data.key).child('Formative').child(i).once("value");
Promise.all([xIndexRef, yIndexRef])
.then(results => {
const xSnapshot = results[0];
const ySnapshot = results[1];
return admin.database().ref('StudentPlacement').child(data.key).child(xSnapshot.val() + ":" + ySnapshot.val()).once("value");
})
.then(snapshot => {
console.log("Student Placement is: ", snapshot.val());
});
}
Promise.all waits for both xIndexRef and yIndexRef to complete their execution.
Once executed the results are returned into a thenable object.
You can access the results and complete your execution.

once("child_added") on non existing node does not fail but timeout

First I'm not sure that there is a real problem but I guess I'll share my reasoning.
I use Firebase as a database / backend for the archiving of all the data from various sensors at home and an UI with cool graphs in hosting. So every 10 minutes I push various data (temperature, humidity, CO2 level, illumination, ...) coming from various rooms. I have almost 3 years of data available (so my base has a lots of nodes)
So my database structure is like that :
root
readings
room_id
GUID
time
temp
hum
lum
For a few years I had a PHP script hosted at home that checked if the latest item inside each readings/room_id has a time value that is not too old (no more than 11 minutes old). I translated it to Firebase cloud function some days ago and I got something like this :
exports.monitor = functions.https.onRequest((req, res) => {
const tstamp = Math.floor(Date.now() / 1000);
var sensors = ["r01", "r02", "r03", "r04", "r05"];
var promiseArray = [];
var result = {};
for (var i = 0; i < sensors.length; i++) {
console.log('Adding promise for ' + sensors[i]);
promiseArray.push(admin.database().ref('/readings/' + sensors[i]).limitToLast(1).once("child_added"));
}
Promise.all(promiseArray).then(snapshots => {
console.log('All promises done : ' + snapshots.length);
res.set('Cache-Control', 'private, max-age=300');
for (var i = 0; i < snapshots.length; i++) {
differenceInMinutes = (tstamp - snapshots[i].val().time) / 60;
result[sensors[i]] = {current: tstamp,
sensor: snapshots[i].val().time,
diff: Math.round(differenceInMinutes * 10) / 10};
if (differenceInMinutes < 11) {
result[sensors[i]]['status'] = "OK";
} else {
result[sensors[i]]['status'] = "KO";
}
}
return res.status(200).json(result);
}).catch(error => {
console.error('Error while getting sensors details', error.message);
res.sendStatus(500);
});
});
The code works well. So my question is : if I add another room ID in the sensors array that does not exists inside "readings" in my database, I thought I'll get an error (failed promise) instead I only got a huge timeout error, I don't want that kind of timeout on Firebase Cloud Functions (to avoid any unwanted cost).
Is that normal ? Is my code wrong ? Do I have to start by getting a shallow snapshot of "readings/room_id" check that it exists and check if has children ?
Thanks a lot for your help.
EDIT : With the help of Frank I fixed my code, here is the revised version :
exports.monitor = functions.https.onRequest((req, res) => {
const tstamp = Math.floor(Date.now() / 1000);
var sensors = ["r01", "r02", "r03", "r04", "r05"];
var promiseArray = [];
var result = {};
for (var i = 0; i < sensors.length; i++) {
console.log('Adding promise for ' + sensors[i]);
promiseArray.push(admin.database().ref('/readings/' + sensors[i]).limitToLast(1).once("value"));
}
Promise.all(promiseArray).then(queryResults => {
console.log('All promises done : ' + queryResults.length);
res.set('Cache-Control', 'private, max-age=300');
queryResults.forEach((snapshots, i) => {
snapshots.forEach((snapshot) => {
var currentData = snapshot.val();
differenceInMinutes = (tstamp - currentData.time) / 60;
result[sensors[i]] = {current: tstamp,
sensor: currentData.time,
diff: Math.round(differenceInMinutes * 10) / 10};
if (differenceInMinutes < 11) {
result[sensors[i]]['status'] = "OK";
} else {
result[sensors[i]]['status'] = "KO";
}
});
});
return res.status(200).json(result);
}).catch(error => {
console.error('Error while getting sensors details', error.message);
res.sendStatus(500);
});
});
a child_added event only fires when there is a child node. If there are not child nodes under the location (or matching the query) it will not fire.
To ensure you also get notified in the condition there are no children, you should listen to the value event:
for (var i = 0; i < sensors.length; i++) {
console.log('Adding promise for ' + sensors[i]);
var query = admin.database().ref('/readings/' + sensors[i]).limitToLast(1).once("value")
promiseArray.push(query);
}
Since a value event may match multiple children in a single snapshot (despite your query only requesting a single child), you will need to loop over the children of the resulting snapshot:
Promise.all(promiseArray).then((queryResults) => {
console.log('All promises done : ' + queryResults.length);
res.set('Cache-Control', 'private, max-age=300');
queryResults.forEach((snapshots) => {
snapshots.forEach((snapshot) => {
differenceInMinutes = (tstamp - snapshot.val().time) / 60;
...

Categories