Code afterwards getting executed first in Firebase cloud function - javascript

Here is my code
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendMessage = functions.database.ref('/UserRequests/{uid}')
.onCreate((snap, context) => {
const position = snap.val();
var loc = [position.l[0], position.l[1]];
const db = admin.database();
const ref = db.ref('/DriversAvailable');
const drivers = new GeoFire(ref);
var data = [];
const pathId = context.auth.uid;
const ref1 = db.ref('/UserRequests/{pathId}');
console.log("UserID" + pathId);
ref.once('value').then(snapshot => {
snapshot.forEach(function (child) {
console.log(child.key + " key"); //This code gets executed afterwards.
var c = child.val();
var aaa = child.key;
var lat = c.l[0];
var lng = c.l[1];
var dist = getDistance(position.l[0], position.l[1], lat, lng);
console.log("dis" + lat + lng + "aaa" + dist);
data.push({
id: aaa,
dis: dist
});
});
return console.log("gotit");
});
var getDistance = function (lat1, lng1, lat2, lng2) {
var R = 6378137; // Earth’s mean radius in meter
var dLat = rad(lat2 - lat1);
var dLong = rad(lng2 - lng1);
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(rad(lat1)) * Math.cos(rad(lat2)) *
Math.sin(dLong / 2) * Math.sin(dLong / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c;
return d; // returns the distance in meter
};
var rad = function (x) {
return x * Math.PI / 180;
};
data.sort(function (a, b) {
return a.dis - b.dis
});
var i = 0;
var n = data.length;
console.log("number" + n); //This code is executed first.
while (i < 2 && i <= n) {
i++;
var k = data[i].id;
const getdevicetokenpromise = db.ref(`/DriversAvailable/${k}/token`).once('value');
return getdevicetokenpromise.then(result => {
console.log(result.val());
var token = result.val();
const payload = {
data: {
uid: pathId
}
};
return admin.messaging().sendToDevice(token, payload)
.then((response) => {
return console.log("Successfully sent message:", response);
})
.catch((error) => {
console.log("Error sending message:", error);
});
});
}
console.log("hi");
});
I am deploying the above function in Firebase cloud. I have commented about which code is being executed first and which is executed afterwards. I couldn't get why this is happening.
The part which is executed afterwards is taking data from firebase and computing the distance between two points using the function getDistance. As it comes before the code below it, it should be executed first.

Firebase works with promises, whereby the result is either resolved or rejected.
that means that the result, in your case the data fetched, takes sometime. Since your code is not nested, any other functions are executed asyncronously
I suggest nesting what you need executed within the .then{} block, or decoupling the functionality and putting it in a separate function, then calling that function while passing relevant parameters, in this case the snapshot data

The call ref.on("value", callback) will be executed first. The callback however will be executed somewhere in the future.
In you case you are probably better of using once:
ref.once('value').then(snapshot => ...)
All other logic should live inside the then as it depends on the value in the snapshot:
return ref.once('value).then(snapshot => {
var data = []
...
snapshot.forEach(child => {
data.push(...)
})
...
// other code
})

Related

Cloud function does not write second argument to Firebase

I have written a cloud function that runs every 5 minutes on my Firebase app. In essence, the function gathers trends data from the Google Trends website and parses the JSON into a variable.
After doing so I want to then connect to the Twitter API and search for tweets using the trending topics fetched in the first part.
My Issue seems to lie with the second part. It fetches the data but the remainder of the function does not wait for the result before writing to Firebase.
I have tried two different methods but both don't seem to work as intended. I am struggling to understand how the function should wait for the second part to gather and store the information before writing to Firebase.
Method 1
exports.callTo = functions.pubsub.schedule("5 * * * *").onRun((context) => {
let searchTrends;
const ts = Date.now();
const dateOb = new Date(ts);
const date = dateOb.getDate();
const month = dateOb.getMonth() + 1;
const year = dateOb.getFullYear();
const twitterTrends = [];
googleTrends.dailyTrends({
trendDate: new Date(year + "-" + month + "-" + date),
geo: "CA",
}, function(err, res) {
if (err) {
functions.logger.error(err);
} else {
searchTrends = JSON.parse(res).default.trendingSearchesDays[0]
.trendingSearches;
functions.logger.info(searchTrends);
for (let i = 0; i < searchTrends.length; i++) {
functions.logger.log(searchTrends[i].title.query);
T.get("search/tweets", {q: searchTrends[i].title.query, count: 1},
function(err, data, response) {
if (err) {
functions.logger.error(err);
}
functions.logger.info("Twitter data" +
JSON.stringify(data.statuses));
twitterTrends[i] = JSON.stringify(data.statuses);
});
}
const dbRef = admin.database().ref("searchTrends");
dbRef.set({google: searchTrends, twitter: twitterTrends});
}
});
});
Method 2
exports.callTo = functions.pubsub.schedule("5 * * * *").onRun((context) => {
let searchTrends;
const ts = Date.now();
const dateOb = new Date(ts);
const date = dateOb.getDate();
const month = dateOb.getMonth() + 1;
const year = dateOb.getFullYear();
const twitterTrends = [];
async function getTrends(){
googleTrends.dailyTrends({
trendDate: new Date(year + "-" + month + "-" + date),
geo: "CA",
}, function(err, res) {
if (err) {
functions.logger.error(err);
} else {
searchTrends = JSON.parse(res).default.trendingSearchesDays[0]
.trendingSearches;
functions.logger.info(searchTrends);
}
});
await getTwitterTrends();
}
async function getTwitterTrends(){
for (let i = 0; i < 1; i++) {
functions.logger.log(searchTrends[i].title.query);
T.get("search/tweets", {q: searchTrends[i].title.query, count: 1},
function(err, data, response) {
if (err) {
functions.logger.error(err);
} else {
functions.logger.info("Twitter data" +
JSON.stringify(data.statuses));
twitterTrends[i] = JSON.stringify(data.statuses);
}
});
}
return "done";
}
const dbRef = admin.database().ref("searchTrends");
dbRef.set({google: searchTrends, twitter: twitterTrends});
});
After checking your function it looks like a Promises issue. The reason you are seeing only the searchTrends data in Firestore is because the Firestore reference and upload is being done inside the callback for the dailyTrends method (taking for reference the method 1 code). However this does not wait for each request to the Twitter API to be resolved before writing to Firestore.
Based on the documentation for twit (which seems to be the wrapper you are using), it also supports standard promises. You could add each promise to an array, and then use Promise.all() to wait until they are all resolved to then write the data into Firestore. It would look something like this (which I haven’t tested since I don’t have Twitter API access).
exports.callTo = functions.pubsub.schedule("5 * * * *").onRun((context) => {
const ts = Date.now();
const dateOb = new Date(ts);
const date = dateOb.getDate();
const month = dateOb.getMonth() + 1;
const year = dateOb.getFullYear();
let searchTrends;
const twitterTrends = [];
const twPromises = [];
googleTrends.dailyTrends({
trendDate: new Date(year + "-" + month + "-" + date),
geo: "CA",
}, function(err, res) {
if (err) {
functions.logger.error(err);
} else {
searchTrends = JSON.parse(res).default.trendingSearchesDays[0]
.trendingSearches;
functions.logger.info(searchTrends);
for (let i = 0; i < searchTrends.length; i++) {
functions.logger.log(searchTrends[i].title.query);
twPromises.push(T.get("search/tweets", {q: searchTrends[i].title.query, count: 1})); // adds promises to the array
}
Promise.all(twPromises).then((responses) => { // runs when all promises from the array are resolved
responses.forEach((response) => {
twitterTrends.push(JSON.stringify(response.statuses));
})
const dbRef = admin.database().ref("searchTrends");
dbRef.set({google: searchTrends, twitter: twitterTrends});
})
}
});
});

getting a return value out of an async expression

How can I get a return value from the following code? I don't want to just console log it
function get_disk_space(callback){
checkDiskSpace('C:\\').then((diskSpace) => {
var free = Math.floor(diskSpace.free/1000000000)
var size = Math.floor(diskSpace.size/1000000000)
var used = size - free
percent = (size - free)
percent = percent/size*100
percent = Math.floor(percent)+'%'
return percent;
});
}
running this will return undefined while running a console log just before return percent will give me the correct answer..
You can simply add a then-handler to process the resolved value, e.g:
function get_disk_space(callback) {
return checkDiskSpace('C:\\').then((diskSpace) => {
var free = Math.floor(diskSpace.free / 1000000000)
var size = Math.floor(diskSpace.size / 1000000000)
var used = size - free
percent = (size - free)
percent = percent / size * 100
percent = Math.floor(percent) + '%'
return percent;
});
}
In order to access the result outside the get_disk_space function scope, you need to do:
get_disk_space().then((result) => {
//log and handle your result
console.log(result);
// handle your result
});
In case the function where you call get_disk_space is async, you can simply do:
async function testFunc() {
const result = await get_disk_space();
// handle result
}

How to send Javascript list to angular controller

What I am trying to achieve is send a list of zip codes from my JavaScript function that gets a range based on the distance and zip code that the user enters. Once I get my list of zip codes, send the list to my angular controller.
Originally, the scope below would grab the zip code that was entered by the user from the form (see below):
$scope.GetCurrentZip = function (){
try{
$http.get("../remote/ReturnZipCodes.cfm")
.then(function(response){
$scope.searchParam.Zip = response.data;
})
}
else{ console.log('No geolocation'); }
}
catch(err) { console.log(err.message); }
}
I would like to use the same scope that I have above. However, instead of grabbing the value that was entered by the user, I would like to grab the list that I obtain from the function instead.
The following is the JavaScript function that generates the list of zip codes:
var rad = function(x) {
return x * Math.PI / 180;
};
var getDistance = function(p1, p2) {
var R = 6378137; // Earth’s mean radius in meter
var dLat = rad(p2.lat - p1.lat);
var dLong = rad(p2.lon - p1.lon);
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(rad(p1.lat)) * Math.cos(rad(p2.lat)) * Math.sin(dLong / 2) * Math.sin(dLong / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c;
return d; // returns the distance in meter
};
function getZipCodes() {
var miles = document.getElementById("miles").options[document.getElementById("miles").selectedIndex].innerHTML;
var zip = document.getElementById("zip").value;
if (typeof zip === 'undefined' || typeof miles === 'undefined' || !zip.length || !miles.length) return false;
var zips = getZips();
var zip_list = "";
if (zips.length) {
// var list_item = "";
// for (i=0;i<zips.length;i++) {
// console.log(zips[i]);
// }
zip_list = zips.join();
}
return zip_list;
}
function getZips() {
var zipcode1 = getZipInfo(zip);
var res = [];
if (typeof zipcode1 === "undefined") return false;
for (i=0; i<zipinfo.length; i++) {
var zipcode2 = zipinfo[i];
var distance = getDistance(zipcode1, zipcode2);
//Convert meters into miles
distance = distance/1609.344;
if (distance <= miles) {
res.push(zipcode2.zip);
}
}
return res;
}
function getZipInfo(zip) {
for (i=0; i<zipinfo.length; i++) {
if (zipinfo[i].zip == zip) return zipinfo[i];
}
}
Any help would be appreciated.
Update: As stated by #charlietfl, I was not clear with what I was asking in my question and I do apologize for any confusion.
What I was asking is I would like to retrieve the list of zip codes that was generated in my JavaScript function GetZips().
As suggested by #charlietfl (thanks for your help once more), all I needed to do is the following:
$scope.GetCurrentZip = function (){
try{
$scope.Zip = getZips();
}
catch(err) {}
}
Update: #LSD pointed out that it will not pass do to the values passing in are not in a array. Is it possible to change the scope to accept the list? The list is a string.
Below code of angular will give you the hiden value
var hidenctl = angular.element(document.querySelector('#hidenid'));
var hidenvalue = hidenctl.val();

Illegal return statement in JavaScript

A 'days' node with more than 1 child isn't getting removed. How can I fix this issue?
I need to ensure that my promise bubbles up to the last then() on the top-level. So I need a return before collectionRef.once. But that return statement now prevents the collectionRef.once from happening. I'm stuck!
Here's my code
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const defaultDatabase = admin.database();
exports.deleteOldItems = functions.database.ref('/path/to/items/{pushId}')
.onWrite(event => {
var ref = event.data.ref.parent; // reference to the items
var now = Date.now();
var cutoff = now - 2 * 60 * 60 * 1000;
var oldItemsQuery = ref.orderByChild('timestamp').endAt(cutoff);
return oldItemsQuery.once('value', function(snapshot) {
// create a map with all children that need to be removed
var updates = {};
snapshot.forEach(function(child) {
updates[child.key] = null
});
// execute all updates in one go and return the result to end the function
return ref.update(updates);
}).then(function() {;
const theRef = event.data.ref;
const collectionRef = theRef.parent.child('days');
return collectionRef; // ILEGAL RETURN STATEMENT
collectionRef.once('value').then(messagesData => {
if(messagesData.numChildren() > 1) {
let updates = {};
updates['/days'] = null;
return defaultDatabase.ref().update(updates); // 'days' doesn't get removed even if it has more than 1 child (as in the image)!
}
})
});
});
Data structure: https://i.stack.imgur.com/gVn8S.jpg
exports.deleteOldItems = functions.database.ref('/path/to/items/{pushId}')
.onWrite(event => {
var ref = event.data.ref.parent // reference to the items
var now = Date.now()
var cutoff = now - 2 * 60 * 60 * 1000
var oldItemsQuery = ref.orderByChild('timestamp').endAt(cutoff)
return oldItemsQuery.once('value', function(snapshot) {
// create a map with all children that need to be removed
var updates = {}
snapshot.forEach(function(child) {
updates[child.key] = null
})
// execute all updates in one go and return the result to end the function
return ref.update(updates)
}).then(function() {
// const theRef = event.data.ref
const collectionRef = defaultDatabase.ref().child('/days')
// return collectionRef // ILEGAL RETURN STATEMENT
collectionRef.once('value').then(messagesData => {
console.log(`Hello messageData : ${messagesData.numChildren()}`)
if(messagesData.numChildren() > 1) {
const updates = {}
updates['/days'] = null
return defaultDatabase.ref().update(updates); // 'days' doesn't get removed even if it has more than 1 child (as in the image)!
}
})
})
use defaultDatabase.ref().child('/days') instead of using event.data.ref.parent
also please go through the documentation and learn how promises works it will help you future. For now these changes will work.Tested at my end.
videos you must watch
What's the difference between event.data.ref and event.data.adminRef? - #AskFirebase
Asynchronous Programming (I Promise!) with Cloud Functions for Firebase - Firecasts
you can subscribe their Firebase YouTube Channel to get latest updates and learn More.

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