Currently, I am trying to get the md5 of every value in array. Essentially, I loop over every value and then hash it, as such.
var crypto = require('crypto');
function userHash(userIDstring) {
return crypto.createHash('md5').update(userIDstring).digest('hex');
}
for (var userID in watching) {
refPromises.push(admin.database().ref('notifications/'+ userID).once('value', (snapshot) => {
if (snapshot.exists()) {
const userHashString = userHash(userID)
console.log(userHashString.toUpperCase() + "this is the hashed string")
if (userHashString.toUpperCase() === poster){
return console.log("this is the poster")
}
else {
..
}
}
else {
return null
}
})
)}
However, this leads to two problems. The first is that I am receiving the error warning "Don't make functions within a loop". The second problem is that the hashes are all returning the same. Even though every userID is unique, the userHashString is printing out the same value for every user in the console log, as if it is just using the first userID, getting the hash for it, and then printing it out every time.
Update LATEST :
exports.sendNotificationForPost = functions.firestore
.document('posts/{posts}').onCreate((snap, context) => {
const value = snap.data()
const watching = value.watchedBy
const poster = value.poster
const postContentNotification = value.post
const refPromises = []
var crypto = require('crypto');
function userHash(userIDstring) {
return crypto.createHash('md5').update(userIDstring).digest('hex');
}
for (let userID in watching) {
refPromises.push(admin.database().ref('notifications/'+ userID).once('value', (snapshot) => {
if (snapshot.exists()) {
const userHashString = userHash(userID)
if (userHashString.toUpperCase() === poster){
return null
}
else {
const payload = {
notification: {
title: "Someone posted something!",
body: postContentNotification,
sound: 'default'
}
};
return admin.messaging().sendToDevice(snapshot.val(), payload)
}
}
else {
return null
}
})
)}
return Promise.all(refPromises);
});
You have a couple issues going on here. First, you have a non-blocking asynchronous operation inside a loop. You need to fully understand what that means. Your loop runs to completion starting a bunch of non-blocking, asynchronous operations. Then, when the loop finished, one by one your asynchronous operations finish. That is why your loop variable userID is sitting on the wrong value. It's on the terminal value when all your async callbacks get called.
You can see a discussion of the loop variable issue here with several options for addressing that:
Asynchronous Process inside a javascript for loop
Second, you also need a way to know when all your asynchronous operations are done. It's kind of like you sent off 20 carrier pigeons with no idea when they will all bring you back some message (in any random order), so you need a way to know when all of them have come back.
To know when all your async operations are done, there are a bunch of different approaches. The "modern design" and the future of the Javascript language would be to use promises to represent your asynchronous operations and to use Promise.all() to track them, keep the results in order, notify you when they are all done and propagate any error that might occur.
Here's a cleaned-up version of your code:
const crypto = require('crypto');
exports.sendNotificationForPost = functions.firestore.document('posts/{posts}').onCreate((snap, context) => {
const value = snap.data();
const watching = value.watchedBy;
const poster = value.poster;
const postContentNotification = value.post;
function userHash(userIDstring) {
return crypto.createHash('md5').update(userIDstring).digest('hex');
}
return Promise.all(Object.keys(watching).map(userID => {
return admin.database().ref('notifications/' + userID).once('value').then(snapshot => {
if (snapshot.exists()) {
const userHashString = userHash(userID);
if (userHashString.toUpperCase() === poster) {
// user is same as poster, don't send to them
return {response: null, user: userID, poster: true};
} else {
const payload = {
notification: {
title: "Someone posted something!",
body: postContentNotification,
sound: 'default'
}
};
return admin.messaging().sendToDevice(snapshot.val(), payload).then(response => {
return {response, user: userID};
}).catch(err => {
console.log("err in sendToDevice", err);
// if you want further processing to stop if there's a sendToDevice error, then
// uncomment the throw err line and remove the lines after it.
// Otherwise, the error is logged and returned, but then ignored
// so other processing continues
// throw err
// when return value is an object with err property, caller can see
// that that particular sendToDevice failed, can see the userID and the error
return {err, user: userID};
});
}
} else {
return {response: null, user: userID};
}
});
}));
});
Changes:
Move require() out of the loop. No reason to call it multiple times.
Use .map() to collect the array of promises for Promise.all().
Use Object.keys() to get an array of userIDs from the object keys so we can then use .map() on it.
Use .then() with .once().
Log sendToDevice() error.
Use Promise.all() to track when all the promises are done
Make sure all promise return paths return an object with some common properties so the caller can get a full look at what happened for each user
These are not two problems: the warning you get is trying to help you solve the second problem you noticed.
And the problem is: in Javascript, only functions create separate scopes - every function you define inside a loop - uses the same scope. And that means they don't get their own copies of the relevant loop variables, they share a single reference (which, by the time the first promise is resolved, will be equal to the last element of the array).
Just replace for with .forEach.
Related
I want my while loop to match 2 conditions in order to break the loop - res has to NOT be undefined, which is the case only when status code is 200, and obj.owner has to be equal to some value i set. It takes a couple of seconds for the owner on that page to update, so i need the while loop to run until the owner updates.
Here's the request function
const getOwner = () => {
const opts = {
url: 'https://example.com',
json: true,
simple: false,
resolveWithFullResponse: true
}
return request(opts)
.then(res => {
if (res.statusCode == 200) {
const obj = {
owner: res.body.owner
}
return obj
} else {
console.error(`Error: ${res.statusCode}`)
}
})
.catch(e => {
console.error(`Panic: ` + e.message)
})
}
And here's the while loop
let owner = await getOwner()
while (!owner && owner.owner != 'some value') {
owner = await getOwner()
}
console.log('Loop broken')
When I console.log(!owner, (owner.owner != 'some value')) after the first function call, before trying to enter into the loop, it returns false for the first condition, and true for the second.
When status code is other than 200, it doesnt return the object, so i need those 2 conditions together, as otherwise it crashes the program due to owner being undefined.
Now, when i execute this, it always skips the loop no matter what, im really lost at this point.
In my opinion, it's a bad idea to loop through a resource until you get the intended result you are seeking.
If you have control over the resource that you are calling, change it to return the result according to your expectation.
If you do not have control over the resource, in my opinion, the resource should not be return different results on different calls. If you get a different response on each call, then the resource is not idempotent. It's a bad design.
Imagine, you would be looping n times, with that you may or may not get the results you are looking for.
I would suggest looking into implementation and fixing the root of the problem, rather than work around.
getOwner returns a Promise. You will need to await for it. And I don't think you really need the while loop:
let owner = await getOwner();
if(owner && owner.owner != 'some value') {
/*do stuff with owner*/
}
console.log(owner);
In my code below I get an empty array on my console.log(response) but the console.log(filterdIds) inside the getIds function is showing my desired data. I think my resolve is not right.
Note that I run do..while once for testing. The API is paged. If the records are from yesterday it will keep going, if not then the do..while is stopped.
Can somebody point me to the right direction?
const axios = require("axios");
function getToken() {
// Get the token
}
function getIds(jwt) {
return new Promise((resolve) => {
let pageNumber = 1;
const filterdIds = [];
const config = {
//Config stuff
};
do {
axios(config)
.then((response) => {
response.forEach(element => {
//Some logic, if true then:
filterdIds.push(element.id);
console.log(filterdIds);
});
})
.catch(error => {
console.log(error);
});
} while (pageNumber != 1)
resolve(filterdIds);
});
}
getToken()
.then(token => {
return token;
})
.then(jwt => {
return getIds(jwt);
})
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
I'm also not sure where to put the reject inside the getIds function because of the do..while.
The fundamental problem is that resolve(filterdIds); runs synchronously before the requests fire, so it's guaranteed to be empty.
Promise.all or Promise.allSettled can help if you know how many pages you want up front (or if you're using a chunk size to make multiple requests--more on that later). These methods run in parallel. Here's a runnable proof-of-concept example:
const pages = 10; // some page value you're using to run your loop
axios
.get("https://httpbin.org") // some initial request like getToken
.then(response => // response has the token, ignored for simplicity
Promise.all(
Array(pages).fill().map((_, i) => // make an array of request promisess
axios.get(`https://jsonplaceholder.typicode.com/comments?postId=${i + 1}`)
)
)
)
.then(responses => {
// perform your filter/reduce on the response data
const results = responses.flatMap(response =>
response.data
.filter(e => e.id % 2 === 0) // some silly filter
.map(({id, name}) => ({id, name}))
);
// use the results
console.log(results);
})
.catch(err => console.error(err))
;
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
The network tab shows the requests happening in parallel:
If the number of pages is unknown and you intend to fire requests one at a time until your API informs you of the end of the pages, a sequential loop is slow but can be used. Async/await is cleaner for this strategy:
(async () => {
// like getToken; should handle err
const tokenStub = await axios.get("https://httpbin.org");
const results = [];
// page += 10 to make the snippet run faster; you'd probably use page++
for (let page = 1;; page += 10) {
try {
const url = `https://jsonplaceholder.typicode.com/comments?postId=${page}`;
const response = await axios.get(url);
// check whatever condition your API sends to tell you no more pages
if (response.data.length === 0) {
break;
}
for (const comment of response.data) {
if (comment.id % 2 === 0) { // some silly filter
const {name, id} = comment;
results.push({name, id});
}
}
}
catch (err) { // hit the end of the pages or some other error
break;
}
}
// use the results
console.log(results);
})();
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
Here's the sequential request waterfall:
A task queue or chunked loop can be used if you want to increase parallelization. A chunked loop would combine the two techniques to request n records at a time and check each result in the chunk for the termination condition. Here's a simple example that strips out the filtering operation, which is sort of incidental to the asynchronous request issue and can be done synchronously after the responses arrive:
(async () => {
const results = [];
const chunk = 5;
for (let page = 1;; page += chunk) {
try {
const responses = await Promise.all(
Array(chunk).fill().map((_, i) =>
axios.get(`https://jsonplaceholder.typicode.com/comments?postId=${page + i}`)
)
);
for (const response of responses) {
for (const comment of response.data) {
const {name, id} = comment;
results.push({name, id});
}
}
// check end condition
if (responses.some(e => e.data.length === 0)) {
break;
}
}
catch (err) {
break;
}
}
// use the results
console.log(results);
})();
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
(above image is an except of the 100 requests, but the chunk size of 5 at once is visible)
Note that these snippets are proofs-of-concept and could stand to be less indiscriminate with catching errors, ensure all throws are caught, etc. When breaking it into sub-functions, make sure to .then and await all promises in the caller--don't try to turn it into synchronous code.
See also
How do I return the response from an asynchronous call? and Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference which explain why the array is empty.
What is the explicit promise construction antipattern and how do I avoid it?, which warns against adding a new Promise to help resolve code that already returns promises.
To take a step back and think about why you ran into this issue, we have to think about how synchronous and asynchronous javascript code works together. Your
synchronous getIds function is going to run to completion, stepping through each line until it gets to the end.
The axios function invocation is returning a Promise, which is an object that represents some future fulfillment or rejection value. That Promise isn't going to resolve until the next cycle of the event loop (at the earliest), and your code is telling it to do some stuff when that pending value is returned (which is the callback in the .then() method).
But your main getIds function isn't going to wait around... it invokes the axios function, gives the Promise that is returned something to do in the future, and keeps going, moving past the do/while loop and onto the resolve method which returns a value from the Promise you created at the beginning of the function... but the axios Promise hasn't resolved by that point and therefore filterIds hasn't been populated.
When you moved the resolve method for the promise you're creating into the callback that the axios resolved Promise will invoke, it started working because now your Promise waits for axios to resolve before resolving itself.
Hopefully that sheds some light on what you can do to get your multi-page goal to work.
I couldn't help thinking there was a cleaner way to allow you to fetch multiple pages at once, and then recursively keep fetching if the last page indicated there were additional pages to fetch. You may still need to add some additional logic to filter out any pages that you batch fetch that don't meet whatever criteria you're looking for, but this should get you most of the way:
async function getIds(startingPage, pages) {
const pagePromises = Array(pages).fill(null).map((_, index) => {
const page = startingPage + index;
// set the page however you do it with axios query params
config.page = page;
return axios(config);
});
// get the last page you attempted, and if it doesn't meet whatever
// criteria you have to finish the query, submit another batch query
const lastPage = await pagePromises[pagePromises.length - 1];
// the result from getIds is an array of ids, so we recursively get the rest of the pages here
// and have a single level array of ids (or an empty array if there were no more pages to fetch)
const additionalIds = !lastPage.done ? [] : await getIds(startingPage + pages, pages);
// now we wait for all page queries to resolve and extract the ids
const resolvedPages = await Promise.all(pagePromises);
const resolvedIds = [].concat(...resolvedPages).map(elem => elem.id);
// and finally merge the ids fetched in this methods invocation, with any fetched recursively
return [...resolvedIds, ...additionalIds];
}
This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 4 years ago.
I need to fetch id of user from collection 'users' by calling a function and return it's value.
fetchId = (name) => {
User.findOne({name: name}, (err, user) => {
return user._id;
});
};
But this implementation returns null. What is the way to fix it?
following your example, if you don't want to use promises, you can simply pass a callback from the caller and invoke the callback when you have the result since the call to mongo is asynchronous.
fetchId = (name, clb) => {
User.findOne({name: name}, (err, user) => {
clb(user._id);
});
};
fetchId("John", id => console.log(id));
Otherwise you can use the promise based mechanism omitting the first callback and return the promise to the caller.
fetchId = name => {
return User.findOne({name: name}).then(user => user.id);
};
fetchId("John")
.then(id => console.log(id));
A third way is a variation of suggestion #2 in #Karim's (perfectly good) answer. If the OP wants to code it as if results are being assigned from async code, an improvement would be to declare fetchId as async and await its result...
fetchId = async (name) => {
return User.findOne({name: name}).then(user => user.id);
};
let someId = await fetchId("John");
console.log(id)
edit
For any method on an object -- including a property getter -- that works asynchronously, the calling code needs to be aware and act accordingly.
This tends to spread upward in your system to anything that depends on caller's callers, and so on. We can't avoid this, and there's no syntactic fix (syntax is what the compiler sees). It's physics: things that take longer, just take longer. We can use syntax to partially conceal the complexity, but we're stuck with the extra complexity.
Applying this to your question, say we have an object representing a User which is stored remotely by mongo. The simplest approach is to think of the in-memory user object as unready until an async fetch (findOne) operation has completed.
Under this approach, the caller has just one extra thing to remember: tell the unready user to get ready before using it. The code below employs async/await style syntax which is the the most modern and does the most to conceal -- but not eliminate :-( -- the async complexity...
class MyMongoUser {
// after new, this in-memory user is not ready
constructor(name) {
this.name = name;
this.mongoUser = null; // optional, see how we're not ready?
}
// callers must understand: before using, tell it to get ready!
async getReady() {
this.mongoUser = await myAsyncMongoGetter();
// if there are other properties that are computed asynchronously, do those here, too
}
async myAsyncMongoGetter() {
// call mongo
const self = this;
return User.findOne({name: self.name}).then(result => {
// grab the whole remote object. see below
self.mongoUser = result;
});
}
// the remaining methods can be synchronous, but callers must
// understand that these won't work until the object is ready
mongoId() {
return (this.mongoUser)? this.mongoUser._id : null;
}
posts() {
return [ { creator_id: this.mongoId() } ];
}
}
Notice, instead of just grabbing the mongo _id from the user, we put away the whole mongo object. Unless this is a huge memory hog, we might as well have it hanging around so we can get any of the remotely stored properties.
Here's what the caller looks like...
let joe = new MyMongoUser('joe');
console.log(joe.posts()) // isn't ready, so this logs [ { creator_id: null } ];
await joe.getReady();
console.log(joe.posts()) // logs [ { creator_id: 'the mongo id' } ];
I'm trying to send a notification with Firebase Cloud Functions when someone likes a photo. I've coped the Firebase example showing how to send one when someone follows you and tried to modify it.
The problem is that I need to do another additional query in the function to get the key of the person who liked a photo before I can get their token from their User node. The problem is that the getDeviceTokensPromise somehow is not fulfilled and tokensSnapshot.hasChildren can't be read because it is undefined. How can I fix it?
exports.sendPhotoLikeNotification = functions.database.ref(`/photo_like_clusters/{photoID}/{likedUserID}`)
.onWrite((change, context) => {
const photoID = context.params.photoID;
const likedUserID = context.params.likedUserID;
// If un-follow we exit the function
if (!change.after.val()) {
return console.log('User ', likedUserID, 'un-liked photo', photoID);
}
var tripName;
var username = change.after.val()
// The snapshot to the user's tokens.
let tokensSnapshot;
// The array containing all the user's tokens.
let tokens;
const photoInfoPromise = admin.database().ref(`/photos/${photoID}`).once('value')
.then(dataSnapshot => {
tripName = dataSnapshot.val().tripName;
key = dataSnapshot.val().ownerKey;
console.log("the key is", key)
return getDeviceTokensPromise = admin.database()
.ref(`/users/${key}/notificationTokens`).once('value');
///// I need to make sure that this returns the promise used in Promise.all()/////
});
return Promise.all([getDeviceTokensPromise, photoInfoPromise]).then(results => {
console.log(results)
tokensSnapshot = results[0];
// Check if there are any device tokens.
//////This is where I get the error that tokensSnapshot is undefined///////
if (!tokensSnapshot.hasChildren()) {
return console.log('There are no notification tokens to send to.');
}
// Notification details.
const payload = {
notification: {
title: 'Someone liked a photo!',
body: `${username} liked one of your photos in ${tripName}.`,
}
};
// Listing all tokens as an array.
tokens = Object.keys(tokensSnapshot.val());
// Send notifications to all tokens.
return admin.messaging().sendToDevice(tokens, payload);
});
Ok so based on your comment, here's the updated answer.
You need to chain your promises and you don't need to use Promise.all() - that function is used for a collection of asynchronous tasks that all need to complete (in parallel) before you take your next action.
Your solution should look like this:
admin.database().ref(`/photos/${photoID}`).once('value')
.then(dataSnapshot => {
// Extract your data here
const key = dataSnapshot.val().ownerKey;
// Do your other stuff here
// Now perform your next query
return admin.database().ref(`/users/${key}/notificationTokens`).once('value');
})
.then(tokenSnapshot => {
// Here you can use token Snapshot
})
.catch(error => {
console.log(error);
});
Because your second network request depends on the completion of the first, chain your promises by appending a second .then() after your first and don't terminate it with a semi-colon ;. If the new promise created within first .then() scope resolves it will call the second .then() function.
If you need to access variables from the first .then() scope in the second, then you must declare them in the general function, and assign them in the closure scopes.
You can check out more info on promise chaining here.
I have a Cloud Function used to cross reference two lists and find values that match each other across the lists. The function seems to be working properly, however in the logs I keep seeing this Error serializing return value: TypeError: Converting circular structure to JSON . Here is the function...
exports.crossReferenceContacts = functions.database.ref('/cross-ref-contacts/{userId}').onWrite(event => {
if (event.data.previous.exists()) {
return null;
}
const userContacts = event.data.val();
const completionRef = event.data.adminRef.root.child('completed-cross-ref').child(userId);
const removalRef = event.data.ref;
var contactsVerifiedOnDatabase ={};
var matchedContacts= {};
var verifiedNumsRef = event.data.adminRef.root.child('verified-phone-numbers');
return verifiedNumsRef.once('value', function(snapshot) {
contactsVerifiedOnDatabase = snapshot.val();
for (key in userContacts) {
//checks if a value for this key exists in `contactsVerifiedOnDatabase`
//if key dioes exist then add the key:value pair to matchedContacts
};
removalRef.set(null); //remove the data at the node that triggered this onWrite function
completionRef.set(matchedContacts); //write the new data to the completion-node
});
});
I tried putting return in front of completionRef.set(matchedContacts); but that still gives me the error. Not sure what I am doing wrong and how to rid the error. Thanks for your help
I was having the exact same issue when returning multiple promises that were transactions on the Firebase database. At first I was calling:
return Promise.all(promises);
My promises object is an array that I'm using where I'm pushing all jobs that need to be executed by calling promises.push(<add job here>). I guess that this is an effective way of executing the jobs since now the jobs will run in parallel.
The cloud function worked but I was getting the exact same error you describe.
But, as Michael Bleigh suggested on his comment, adding then fixed the issue and I am no longer seeing that error:
return Promise.all(promises).then(() => {
return true;
}).catch(er => {
console.error('...', er);
});
If that doesn't fix your issue, maybe you need to convert your circular object to a JSON format. An example is written here, but I haven't tried that: https://stackoverflow.com/a/42950571/658323 (it's using the circular-json library).
UPDATE December 2017: It appears that in the newest Cloud Functions version, a cloud function will expect a return value (either a Promise or a value), so return; will cause the following error: Function returned undefined, expected Promise or value although the function will be executed. Therefore when you don't return a promise and you want the cloud function to finish, you can return a random value, e.g. return true;
Try:
return verifiedNumsRef.once('value').then(function(snapshot) {
contactsVerifiedOnDatabase = snapshot.val();
for (key in userContacts) {
//checks if a value for this key exists in `contactsVerifiedOnDatabase`
//if key dioes exist then add the key:value pair to matchedContacts
};
return Promise.all([
removalRef.set(null), //remove the data at the node that triggered this onWrite function
completionRef.set(matchedContacts)
]).then(_ => true);
});
I had the same error output with a pretty similar setup and couldn't figure out how to get rid of this error.
I'm not totally sure if every essence has been captured by the previous answers so I'm leaving you my solution, maybe it helps you.
Originally my code looked like this:
return emergencyNotificationInformation.once('value', (data) => {
...
return;
});
But after adding then and catch the error did go away.
return emergencyNotificationInformation.once('value')
.then((data) => {
...
return;
})
.catch((error) => {
...
return:
});
}
We fixed a similar issue with the same error by returning Promise.resolve() at the bottom of the chain, e.g.:
return event.data.ref.parent.child('subject').once('value')
.then(snapshot => {
console.log(snapshot.val());
Promise.resolve();
}).catch(error => {
console.error(error.toString());
});