Order of Async function calls - javascript

I'm making a React App for my Football team to keep track of Payments and Debts for each Member. I'm trying to search the database for Records for each Season and delete a Member if no records are found. Currently the Member gets deleted then the database searched.
This is the code block (it can also be seen with the rest of my App on GitHub here). I use Firebase to store all my data.
import database from '../firebase/firebase';
export const startRemoveMember = ( playerUuid, seasonList ) =>
{
return (dispatch, getState) =>
{
let canDelete = true;
const uid = getState().auth.uid;
// This should resolve first
seasonList.forEach( (season) =>
{
database.ref(`subs-tracker/users/${uid}/debts_and_payments/${season.seasonUuid}`)
.once('value')
.then((records) =>
{
records.forEach((childRecord) =>
{
if(childRecord.val().playerUuid === playerUuid)
{
canDelete = false;
return true; // breaks loop if record is found
};
});
});
});
// This resolves first before block above
if(canDelete)
{
alert('Deleted');
return database.ref(`subs-tracker/users/${uid}/members/${playerUuid}`)
.remove()
.then((ref) =>
{
dispatch(removeMember(playerUuid)); // Calls removeMember() function to remove the Member from the State of the App
})
}
else
{
alert('Cannot Delete. Member has records');
return false;
}
};
};
I'm probably missing something basic. I'm no expert with Databases and Async calls. Any help you would be appreciated :)

In your outer forEach callback you are creating a promise in each iteration. All those promises must resolve first.
You'll have to await all of them.
So instead of doing forEach, use map so you can collect those promises in an array. Then call Promise.all on those, so you have a promise that will resolve when all those have resolved. Then put the deletion code inside a then callback. It cannot obviously be executed synchronously.
This also means you have to think about the return value of your (dispatch, getState) => function. That function cannot give an indication of success in a synchronous way. So it should best return a promise as well.
const promises = seasonList.map( (season) =>
database.ref(`subs-tracker/users/${uid}/debts_and_payments/${season.seasonUuid}`)
.once('value')
.then((records) =>
records.forEach((childRecord) => // breaks on true
childRecord.val().playerUuid === playerUuid
);
);
);
return Promise.all(promises).then(findings =>
findings.includes(true)
).then(cannotDelete => {
if (cannotDelete) {
alert('Cannot Delete. Member has records');
return false;
} else {
alert('Deleted');
return database.ref(`subs-tracker/users/${uid}/members/${playerUuid}`)
.remove()
.then((ref) => {
dispatch(removeMember(playerUuid)); // Calls removeMember() function to remove the Member from the State of the App
return true; // To distinguish from false
});
}
});

Related

Return true from an inner loop

I'm making a React App for my Football team to keep track of Payments and Debts for each Member. I'm trying to search the database for Records and Sessions for each Season and delete a Member if no Records or Sessions are found. Currently it finds Records fine but the Sessions search has an inner loop which I cannot get to return true when a Session is found.
This is the code block (it can also be seen with the rest of my App on GitHub here. I use Firebase to store all my data.
export const startRemoveMember = ( playerUuid, seasonList ) =>
{
return (dispatch, getState) =>
{
const uid = getState().auth.uid;
const recordPromises = seasonList.map( (season) =>
database.ref(`subs-tracker/users/${uid}/debts_and_payments/${season.seasonUuid}`)
.once('value')
.then((records) =>
records.forEach((childRecord) => // breaks on true
childRecord.val().playerUuid === playerUuid
)
)
);
const sessionPromises = seasonList.map( (season) =>
database.ref(`subs-tracker/users/${uid}/sessions/${season.seasonUuid}`)
.once('value')
.then((sessions) =>
sessions.forEach((childSession) =>
childSession.val().playerList.forEach( (player) => // should break on true
player.playerUuid === playerUuid
)
)
)
);
const promises = recordPromises.concat(sessionPromises);
return Promise.all(promises).then(findings =>
findings.includes(true)
)
.then(cannotDelete =>
{
if (cannotDelete)
{
alert('Cannot Delete. Member has records');
return false;
}
else
{
alert('Deleted');
return database.ref(`subs-tracker/users/${uid}/members/${playerUuid}`)
.remove()
.then((ref) =>
{
dispatch(removeMember(playerUuid)); // Calls removeMember() function to remove the Member from the State of the App
return true; // To distinguish from false
}
);
}
});
};
};
I'm probably missing something basic. Any help you would be appreciated :)
forEach does not return anything, for this use case .some is what you need. This iterates over an Array and when it resolves to true it stops iterating and returns true if it resolves to false on every item it will return false

Array.filter() with async arrow function

I am trying to filter my array using Array.filter() function, however I came across this issue.
I need to call some other function inside the filter function asynchronously. However the array is not changing its value based on the conditions that I define in the function.
const filterWithEmail = data.filter(async (doc) =>
{
const findUser = await UserService.findUser(doc.id).catch(err => {});
if (findUser)
{
const { email } = findUser;
return regexFilter ? regexFilter.test(email.normalize("NFKC")) : false;
}
});
This code doesn't affect the data array at all for some reason.
Any suggestions what I'm doing wrong?
Thank you in advance.
filter expects the return value of the callback to be a boolean but async functions always return a promise.
You don't know if you want to return true or false in time to tell filter which it is.
What you possibly want to do is:
map the data from data to { keep: true, original_data: data } (using an async callback)
Pass the resulting array of promises to Promise.all
await the return value of Promise.all
filter that array with: .filter(data => data.keep)
Get the original objects back with .map(data => data.original_data)
Something along these lines (untested):
const filterWithEmail = (
await Promise.all(
data.map(async (data) => {
const findUser = await UserService.findUser(doc.id).catch((err) => {});
let keep = false;
if (findUser && regexFilter)
keep = regexFilter.test(email.normalize("NFKC"));
return { data, keep };
})
)
)
.filter((data) => data.keep)
.map((data) => data.data);

Сollect promises results in foreach loop and only after performing setState

I am new to React and Promises.
My purpose is - Collect array of objects from several Search Services and then transfer whole array to the State.
PNP.SearchResults is a Promise
In code below this.setState is performing earlier then array is ready.
How to fix it?
private getSearchResults() {
const allSearchResults = []
this.state.resultSources.forEach(rSource =>{
this.props.searchService.search(this.props.AdditionalQuery, this.state.activePage, this.props.PageSize, rSource.sourceGuid).then((results: PNP.SearchResults) => {
allSearchResults.push({Results: results, sourceGuid: rSource.sourceGuid});
console.log("push");
}).catch(() => {});
})
this.setState({PrimaryResults2: allSearchResults} as any);
console.log("state updated");
}
Now console.log("state updated") fires earlier then console.log("push").
But I need vice versa
Because of this.props.searchService.search is async function.
You should await result to make sure data return.
private async getSearchResults() {
const allSearchResults = []
for (const rSource of this.state.resultSources) {
await this.props.searchService.search(this.props.AdditionalQuery, this.state.activePage, this.props.PageSize, rSource.sourceGuid).then((results: PNP.SearchResults) => {
allSearchResults.push({Results: results, sourceGuid: rSource.sourceGuid});
console.log("push");
}).catch(() => {});
}
this.setState({PrimaryResults2: allSearchResults} as any);
console.log("state updated");
}
If i understand correctly, you need to first push the search results into allSearchResults array and then setState. Why not use async await and a basic for loop instead of .then. When you use .then, only the code in the .then callback will execute after the promise is resolved but the other code outside it like this.setState, console.log won't wait till you push all the search results.
async func() {
const allSearchResults = [];
for(let i=0; i<this.state.resultSources.length; i+=1){
const item = await this.props.searchService.search(this.props.AdditionalQuery, this.state.activePage, this.props.PageSize, this.state.resultSources[i].sourceGuid)
allSearchResults.push({Results: item, sourceGuid: this.state.resultSources[i].sourceGuid})
}
this.setState({PrimaryResults2: allSearchResults} as any, () => {console.log("state updated")})
}

Run secondary action after promise regardless of outcome?

I found this previous thread (How to perform same action regardless of promise fulfilment?), but it's 5 years old and references winjs is a kludge.
What I would like to do is load a list of data elements. I've got local copies of the list and local copies of the elements -- but they may have changed on the server side.
That process should work like this: load the LIST from the database into the local storage (Comparing against the local) --> THEN load the (multiple) DATA ELEMENTS from the database that are listed in the LIST.
So if the "loadList" async function succeeds... I want to run the "loadElements" async function. If the loadList function rejects... I STILL want to run the "loadElements" function (Which fires off multiple fetch requests - one for each element).
"Use 'finally'" I hear you say... but I want to pass the results of the "loadList" resolve/reject and "loadElements" resolve/reject functions to the calling function. 'finally' doesn't receive or pass properties as far as I know.
The reason I want to pass the results to the calling function is to see if the rejection reasons are acceptable reasons and I can trust the local copy as the authoritative copy or not (for example, if the DB doesn't contain the LIST, I can trust that the local list is the authoritative version)... so I need a way to analyze the 'failures' within the calling function.
Here is what I have:
export function syncLinkTablesAndElementsWithDB(username) {
return (dispatch, getState) => {
return new Promise((resolve, reject) => {
dispatch(loadLinkTableAndElementsFromDB(STATIONS_LINK_TABLE_TYPE, username))
.then((msg) => {
console.log("loadLinkTableAndElementsFromDB RESOLVED: ", msg);
resolve(msg)
})
.then(() => {
dispatch(pushLinkTableToDB(STATIONS_LINK_TABLE_TYPE, username))
dispatch(pushAllUserStationsToDB(username))
})
.catch((allPromReasons) => {
console.log("loadLinkTableAndElementsFromDB REJECTED: ", allPromReasons);
allReasonsAcceptable = true;
allPromReasons.forEach(reason => {
if (!isAcceptableLoadFailureReasonToOverwrite(reason)) {
allReasonsAcceptable = false;
}
});
if (allReasonsAcceptable) {
//TODO: DO push of local to DB
// eventually return results of the push to DB...
} else {
reject(allPromReasons)
}
})
});
}
}
export function loadLinkTableAndElementsFromDB(tableType, username) {
return (dispatch, getState) => {
return new Promise((resolve, reject) => {
dispatch(loadLinkTableFromDB(tableType, username))
.then(successMsg => {
resolve(Promise.all([successMsg, dispatch(loadAllUsersStationsFromDB(username)).catch(err=>err)]))
})
.catch(err => {
reject(Promise.all([err, dispatch(loadAllUsersStationsFromDB(username)).catch(err=>err)]))
})
});
}
}
export function loadAllUsersStationsFromDB(username) {
return (dispatch, getState) => {
return new Promise((resolve, reject) => {
let linkTable = getStationsLinkTable(username); // get the local link table
if (linkTable && Array.isArray(linkTable.stations)) { // if there is a local station list
let loadPromises = linkTable.stations.map(stationID => dispatch(loadStationFromDB(stationID)).catch((err) => err));
Promise.all(loadPromises)
.then((allReasons) => {
let allSuccesses = true;
allReasons.forEach(reason => {
if (!reason.startsWith(SUCCESS_RESPONSE)) {
allSuccesses = false;
}
});
if (allSuccesses) {
resolve(SUCCESS_RESPONSE + ": " + username);
} else {
reject(allReasons);
}
})
} else {
return reject(NO_LINK_TABLE_AVAILABLE + ": " + username);
}
});
};
}
loadStationFromDB and loadLinkTableFromDB do what you'd expect... try to load those things from from the DB. I can include their code if you think it's worthwhile.
----------- EDIT -----------
To clarify what I'm trying to accomplish:
I'm trying to sync local storage with a database. I want to do this by pulling the data from the database, compare the time/datestamps. This will make the local storage version the authoritative copy of all the data. After the loads from the DB, I'd like to then push the local storage version up to the DB.
I need to care for the fact that the database will often simply not have the data at all, and thus might 'reject' on a pull... even though, in the instance of a sync, that rejection is acceptable and should not stop the sync process.
Per suggestions below, I've modified my code:
export function loadLinkTableAndElementsFromDB(tableType, username) {
console.log("loadLinkTableAndElementsFromDB(", tableType, username, ")");
return (dispatch, getState) => {
return new Promise((resolve, reject) => {
dispatch(loadLinkTableFromDB(tableType, username))
.then(successMsg => {
console.log("loadLinkTableFromDB RESOLVED: ", successMsg)
resolve(Promise.all([successMsg, dispatch(loadAllUsersStationsFromDB(username)).catch(err => err)]))
})
.catch(err => {
console.log("loadLinkTableFromDB REJECTED: ", err)
reject(Promise.all([err, dispatch(loadAllUsersStationsFromDB(username)).catch(err => err)]))
})
});
}
}
export function syncLinkTablesAndElementsWithDB(username) {
console.log("syncLinkTablesAndElementsWithDB(", username, ")");
return (dispatch, getState) => {
dispatch(loadLinkTableFromDB(STATIONS_LINK_TABLE_TYPE, username))
.then((successLoadLinkTableMsg) => {
console.log('Successfully loaded link table: ', successLoadLinkTableMsg)
return dispatch(pushLinkTableToDB(STATIONS_LINK_TABLE_TYPE, username))
})
.catch((rejectLoadLinkTableReason) => {
console.log("Failed to load link table from DB: " + rejectLoadLinkTableReason);
if (allReasonsAcceptableForOverwrite(rejectLoadLinkTableReason)) { // some rejection reasons are accectable... so if failed reason is okay....
console.log("Failure to load link table reasons were acceptable... pushing local link table anyway");
return dispatch(pushLinkTableToDB(STATIONS_LINK_TABLE_TYPE, username))
} else {
console.log("Throwing: ", rejectLoadLinkTableReason);
throw rejectLoadLinkTableReason;
}
})
.then((successPushLinkTaleMsg) => {
console.log("Successfully pushed link table: " + successPushLinkTaleMsg);
return dispatch(loadAllUsersStationsFromDB(username)); // I want this to occur regardless of if the link table stuff succeeds or fails... but it must occur AFTER the loadLinkTableFromDB at least tries...
})
.catch((rejectPushLinkTableReason) => {
console.log("Failed to push link table: " + rejectPushLinkTableReason);
return dispatch(loadAllUsersStationsFromDB(username)); // I want this to occur regardless of if the link table stuff succeeds or fails... but it must occur AFTER the loadLinkTableFromDB at least tries...
})
.then((successLoadAllUserStationsMsg) => {
console.log("Successfully loaded all user stations: " + successLoadAllUserStationsMsg);
return dispatch(pushAllUserStationsToDB(username))
})
.catch((rejectLoadAllUserStationsReason) => {
console.log("Failed to push all users stations: " + rejectLoadAllUserStationsReason);
if (allReasonsAcceptableForOverwrite(rejectLoadAllUserStationsReason)) { // some rejection reasons are accectable... so if failed reason is okay....
console.log("Load users stations reasons are acceptable...");
return dispatch(pushAllUserStationsToDB(username))
} else {
console.log("throwing: ", rejectLoadAllUserStationsReason);
throw rejectLoadAllUserStationsReason;
}
})
.then((successPushAllUserStationsMgs) => {
console.log("Successfully pushed all users stations: " + successPushAllUserStationsMgs);
return Promise.resolve();
})
.catch((rejectPushAllUserStationsReason) => {
console.log("Failed to push all users stations: " + rejectPushAllUserStationsReason);
throw rejectPushAllUserStationsReason;
})
};
}
export function syncAllWithDB(username) {
return (dispatch, getState) => {
// other stuff will occur here...
dispatch(syncLinkTablesAndElementsWithDB(username)) // *** Error here ***
.then((successMsg) => {
console.log("Successful sync for : " + successMsg);
})
.catch(allReasons => {
console.warn("Link tables and elements sync error: ", allReasons);
})
// });
}
}
Unfortunately, I'm now getting getting 'TypeError: dispatch(...) is undefined' on the dispatch in the syncAllWithDB function. This function hasn't changed...
I don't entirely follow what you're trying to accomplish (more on that below), but the first thing to do here is to clean up the flow and not wrap an extra new Promise() around existing promises. There is never a reason to do this:
function someFunc() {
return new Promise((resolve, reject) => {
callSomething.then(result => {
...
doSomethingElse(result).then(result2 => {
...
resolve(result2);
}).catch(err => {
...
reject(err);
});
}).catch(err => {
...
reject(err);
});
});
}
That is a well-known promise anti-pattern. You don't need the extra manually created promise wrapped around your function that already makes a promise. Instead, you can just return the promise you already have. This is called "promise chaining". From within the chain you can reject or resolve the chain from anywhere.
function someFunc() {
return callSomething.then(result => {
...
// return promise here, chaining this new async operation
// to the previous promise
return doSomethingElse(result).then(result2 => {
...
return result2;
}).catch(err => {
...
// after doing some processing on the error, rethrow
// to keep the promise chain rejected
throw err;
});
}).catch(err => {
...
reject err;
});
}
Or, you can even flatten the promise chain like this:
function someFunc() {
return callSomething.then(result => {
...
return doSomethingElse(result);
}).then(result2 => {
...
return result2;
}).catch(err => {
...
throw err;
});
}
As an example of that, you can simplify syncLinkTablesAndElementsWithDB() like this:
export function syncLinkTablesAndElementsWithDB(username) {
return (dispatch, getState) => {
return dispatch(loadLinkTableAndElementsFromDB(STATIONS_LINK_TABLE_TYPE, username)).then((msg) => {
console.log("loadLinkTableAndElementsFromDB RESOLVED: ", msg);
dispatch(pushLinkTableToDB(STATIONS_LINK_TABLE_TYPE, username))
dispatch(pushAllUserStationsToDB(username))
// have msg be the resolved value of the promise chain
return(msg);
}).catch((allPromReasons) => {
console.log("loadLinkTableAndElementsFromDB REJECTED: ", allPromReasons);
let allReasonsAcceptable = allPromReasons.every(reason => {
return isAcceptableLoadFailureReasonToOverwrite(reason);
});
if (allReasonsAcceptable) {
//TODO: DO push of local to DB
// eventually return results of the push to DB...
} else {
// have promise stay rejected
throw allPromReasons;
}
});
}
}
As for the rest of your question, you're asking this:
So if the "loadList" async function succeeds... I want to run the "loadElements" async function. If the loadList function rejects... I STILL want to run the "loadElements" function (Which fires off multiple fetch requests - one for each element).
But, there are not functions in your code called loadList() and loadElements() so you lost me there so I'm not sure how to make a specific code suggestion.
Inside a .then() handler in a promise chain, you can do three things:
Return a value. That value becomes the resolved value of the promise chain.
Return a promise. That promise is attached to the promise chain and the whole promise chain (the top-most promise that a caller would be watching) will eventually resolve/reject when this promise you are returning resolves/rejects (or anything that is also chained onto it resolves/rejects).
Throw an exception. All .then() handlers are automatically watched for exceptions and if any exception is throw, then the promise chain is automatically rejected with the exception value set as the reject reason.
So, that gives you the ultimate flexibility to finish the promise chain with a value or an error or to link it to another promise (more asynchronous operations).

Waiting for redux state to meet condition

I have the following function, which is meant to allow me to wait for a particular condition to obtain in my redux state.
async function when(store, condition) {
if (condition(store.getState())) {
return;
} else {
return new Promise(resolve => {
const unsubscribe = store.subscribe(() => {
if (condition(store.getState())) {
unsubscribe();
resolve();
}
});
});
}
}
However, I'm struggling to work out if this is free of race conditions. In particular, if I have a function like...:
async function foo(store) {
await when(store, thereAreExactlyThreeTodoItems);
doSomethingThatRequiresExactlyThreeTodoItems();
}
...am I guaranteed that the condition represented by thereAreExactlyThreeTodoItems() is true when doSomethingThatRequiresExactlyThreeTodoItems() is called? Or does this need a further step to guarantee? i.e.:
async function foo(store) {
do {
await when(store, thereAreExactlyThreeTodoItems);
} while (!thereAreExactlyThreeTodoItems(store.getState());
doSomethingThatRequiresExactlyThreeTodoItems();
}
The worry I have in mind is: could an action be dispatched that invalidates the condition after resolve() is called, but before control is returned to foo()? Unfortunately, I don't have quite a good enough mental model of the javascript event loop to be sure.
Any help much appreciated.
It looks good what you are trying to do, the only thing which seems strange is that you do not cache the promise creation...
I think you have to create a map and cache the created promises so you do not create a new promise for the same functions.
In this case, I don't see a way in which you can have race conditions problems.
Something along this lines:
const cache = {}
function when(store, condition, id) {
if (condition(store.getState())) {
return;
} else if(!cache[id]) {
cache[id] = true;
return new Promise(resolve => {
const unsubscribe = store.subscribe(() => {
if (condition(store.getState())) {
unsubscribe();
resolve();
delete cache[id]
}
});
});
}
}
}

Categories