JS - Promises executing in the wrong order - javascript

what my code is trying to do is create an array of objects that have some dynamic properties, these properties are to be filled as a result of some functions. I'm trying to make use of promises otherwise my template is rendered before the function has finished and these objects' properties will be null or undefined, causing errors in the template.
This is the first function
fetchUserPortfolioCoins({ commit, dispatch, state, rootGetters }) {
const promises = []
promises.push(dispatch('utilities/setLoading', true, { root: true })) // start loader
if (!rootGetters['auth/isAuthenticated']) {
// if user isn't logged, pass whatever is in the store, so apiDetails will be added to each coin
let coins = state.userPortfolioCoins
coins.forEach(coin => { promises.push(dispatch('createAcqCostConverted', coin)) })
commit('SET_USER_COINS', { coins, list: 'userPortfolioCoins' })
} else {
// otherwise, pass the response from a call to the DB coins
Vue.axios.get('/api/coins/').then(response => {
let coins = response.data
coins.forEach(coin => { promises.push(dispatch('createAcqCostConverted', coin)) })
commit('SET_USER_COINS', { coins, list: 'userPortfolioCoins' })
})
}
Promise.all(promises)
.then(() => {
commit('SET_USER_PORTFOLIO_OVERVIEW')
dispatch('utilities/setLoading', false, { root: true })
})
.catch(err => { console.log(err) })
},
that calls this one:
createAcqCostConverted({ dispatch, rootState }, coin) {
const promises = []
// this check is only going to happen for sold coins, we are adding sell_price_converted in case user sold in BTC or ETH
if (coin.sell_currency === 'BTC' || coin.sell_currency === 'ETH') {
const URL = `https://min-api.cryptocompare.com/data/pricehistorical?fsym=${coin.coin_symbol}&tsyms=${rootState.fiatCurrencies.selectedFiatCurrencyCode}&ts=${coin.sold_on_ts}`
promises.push(Vue.axios.get(URL, {
transformRequest: [(data, headers) => {
delete headers.common.Authorization
return data
}]
}))
}
// if user bought with BTC or ETH we convert the acquisition cost to the currently select fiat currency, using the timestamp
if (coin.buy_currency === 'BTC' || coin.buy_currency === 'ETH') {
const URL = `https://min-api.cryptocompare.com/data/pricehistorical?fsym=${coin.coin_symbol}&tsyms=${rootState.fiatCurrencies.selectedFiatCurrencyCode}&ts=${coin.bought_on_ts}`
promises.push(Vue.axios.get(URL, {
transformRequest: [(data, headers) => {
delete headers.common.Authorization
return data
}]
}))
} else {
// if the selected fiatCurrency is the same as the buy_currency we skip the conversion
if (coin.buy_currency === rootState.fiatCurrencies.selectedFiatCurrencyCode) {
coin.acquisition_cost_converted = NaN
return coin
// otherwise we create the acq cost converted property
} else promises.push(dispatch('fiatCurrencies/convertToFiatCurrency', coin, { root: true }))
}
Promise.all(promises)
.then(response => {
const value = response[0].data[coin.coin_symbol][rootState.fiatCurrencies.selectedFiatCurrencyCode]
if (coin.sell_currency === 'BTC' || coin.sell_currency === 'ETH') coin.acquisition_cost_converted = value
if (coin.buy_currency === 'BTC' || coin.buy_currency === 'ETH') coin.acquisition_cost_converted = value
return coin
})
.catch(err => { console.log(err) })
},
The problem is that the first function is not waiting for the second one to complete. How can I adjust this code to fix the issue?
Thanks

You are executing all promises at the same time. Promise.all does not execute them in order. The order of the array you pass it is not relevant. It simply resolves when they all finish regardless of order.
The execution happens when you call the functions, which is before you even push them into the array.
If you need to wait for the first to finish before calling the second. You need to call the second inside the first .then function. For example...
dispatch('utilities/setLoading', true, { root: true }).then(resultOfSetLoading => {
return Promise.all(coins.map(coin => dispatch('createAcqCostConverted', coin)))
}).then(resultOfCreateQcqCostConverted => {
// all createAcqCostConverted are complete now
})
So now, dispatch('utilities/setLoading') will run first. Then, once complete, dispatch('createAcqCostConverted') will run once for each coin (at the same time since I used Promise.all).
I recommend you read up a bit more on how Promise.all works. It is natural to assume it resolves them in order, but it does not.

This is how I managed to make it work (both for logged off user and logged in user, 2 different approaches) after I read some of you guys replies, not sure if it's the cleanest approach.
First function:
fetchUserPortfolioCoins({ commit, dispatch, state, rootGetters }) {
const setCoinsPromise = []
let coinsToConvert = null
// start loader in template
dispatch('utilities/setLoading', true, { root: true })
// if user is logged off, use the coins in the state as dispatch param for createAcqCostConverted
if (!rootGetters['auth/isAuthenticated']) setCoinsPromise.push(coinsToConvert = state.userPortfolioCoins)
// otherwise we pass the coins in the DB
else setCoinsPromise.push(Vue.axios.get('/api/coins/').then(response => { coinsToConvert = response.data }))
// once the call to the db to fetch the coins has finished
Promise.all(setCoinsPromise)
// for each coin retrived, create the converted acq cost
.then(() => Promise.all(coinsToConvert.map(coin => dispatch('createAcqCostConverted', coin))))
.then(convertedCoins => {
// finally, set the portfolio coins and portfolio overview values, and stop loader
commit('SET_USER_COINS', { coins: convertedCoins, list: 'userPortfolioCoins' })
commit('SET_USER_PORTFOLIO_OVERVIEW')
dispatch('utilities/setLoading', false, { root: true })
}).catch(err => { console.log(err) })
},
createAcqCostConverted function:
createAcqCostConverted({ dispatch, rootState }, coin) {
const promises = []
// this check is only going to happen for sold coins, we are adding sell_price_converted in case user sold in BTC or ETH
if (coin.sell_currency === 'BTC' || coin.sell_currency === 'ETH') {
const URL = `https://min-api.cryptocompare.com/data/pricehistorical?fsym=${coin.coin_symbol}&tsyms=${rootState.fiatCurrencies.selectedFiatCurrencyCode}&ts=${coin.sold_on_ts}`
promises.push(Vue.axios.get(URL, {
transformRequest: [(data, headers) => {
delete headers.common.Authorization
return data
}]
}))
}
// if user bought with BTC or ETH we convert the acquisition cost to the currently select fiat currency, using the timestamp
if (coin.buy_currency === 'BTC' || coin.buy_currency === 'ETH') {
const URL = `https://min-api.cryptocompare.com/data/pricehistorical?fsym=${coin.coin_symbol}&tsyms=${rootState.fiatCurrencies.selectedFiatCurrencyCode}&ts=${coin.bought_on_ts}`
promises.push(Vue.axios.get(URL, {
transformRequest: [(data, headers) => {
delete headers.common.Authorization
return data
}]
}))
} else {
// if the selected fiatCurrency is the same as the buy_currency we skip the conversion
if (coin.buy_currency === rootState.fiatCurrencies.selectedFiatCurrencyCode) {
promises.push(coin.acquisition_cost_converted = NaN)
// otherwise we create the acq cost converted property
} else promises.push(dispatch('fiatCurrencies/convertToFiatCurrency', coin, { root: true }))
}
return Promise.all(promises)
.then(response => {
if (coin.sell_currency === 'BTC' || coin.sell_currency === 'ETH') {
const value = response[0].data[coin.coin_symbol][rootState.fiatCurrencies.selectedFiatCurrencyCode]
coin.acquisition_cost_converted = value
}
if (coin.buy_currency === 'BTC' || coin.buy_currency === 'ETH') {
const value = response[0].data[coin.coin_symbol][rootState.fiatCurrencies.selectedFiatCurrencyCode]
coin.acquisition_cost_converted = value
}
return coin
})
.catch(err => { console.log(err) })
},
In the second function I didn't have to adjust much, I just added a "return" for the Promise.all and corrected the if/else to use the response only in specific causes, because the "value" variable generated from the response is valid only in those 2 cases, in the other cases I could simply return the "coin".
Hope it makes sense, here to explain anything better if needed and/or discuss ways to make this code better (I have a feeling it's not, not sure why though :P )

Related

Vuex action not waiting to finish axios promise

I encounter a strange situation developing an application in Laravel + VueJS/Vuex stack.
I understand that if a promise is not returned the parent function calling it will not wait for it to resolve so things will go asynchronous. Axios returns a promise by default when calling a resourse through http.
So i have the parent function which looks like this:
fetchInvoiceSeries() {
var arr = []
let invsrs = this.$store.getters['getInvoiceSeries']
if (invsrs == null) {
return this.$store
.dispatch('get_invoice_series')
.then(() => {
invsrs = this.$store.getters['getInvoiceSeries']
if (invsrs != null) {
invsrs.forEach(function(s) {
arr.push({
value: s.id,
text: s.series + ' / ' + s.increment
})
})
this.series = arr
} else {
console.log('Error while fetching invoice series!')
}
})
.catch(e => {
console.log(e)
})
} else {
invsrs.forEach(function(s) {
arr.push({
value: s.id,
text: s.series + ' / ' + s.increment
})
})
this.series = arr
}
}
And here is the function defined in action part of the vuex module:
get_invoice_series({ commit }) {
return get('/api/series/0')
.then(response => {
if (response.data && typeof response.data !== undefined) {
let payload = response.data
commit('SET_INVOICE_SERIES', payload)
} else {
console.log('error', error)
}
})
.catch(error => {
console.log('error', error)
})
},
So as you can see i am returning the get request from axios inside the action. In the parent i am calling the action and the "then" keyword in order to do some processing after the action it's done. Also i am using arrow function because i need the context in the parent function in order to call this.$store ...
The problem is that even after checking the getter to see if the state have the invoice series and getting them using the get_invoice_series action i still don't have the invoice series in memory judging by the code i wrote. The console keeps loggin 'Error while fetching invoice series!' the first time i execute the code and the second time (after the information exists in state), the code skips fetching the invoice series (as expected).
Can you tell me what i am doing wrong ? Thank you!
Your error comes from invsrs being null the first time, and not null the second time.
This means that your function get_invoice_series({ commit }) is asynchronous, and that it returns a promise.
For more readability, maybe you should make your call independently from your return statement, using async/await expressions :
async get_invoice_series({ commit }) {
const response = await get('/api/series/0')
if (response.data === undefined) return null
const payload = response.data
commit('SET_INVOICE_SERIES', payload)
return payload
},
And then make your calls wait for this fetch to process :
async fetchInvoiceSeries() {
let arr = []
const invsrs = await this.$store.getters['getInvoiceSeries']
// ...
It's pure conjecture here, let me know if it helps or not.

How do I ensure I won't replace a content to an old response?

Good day for all,
I am doing a React course and I'd submited the code to the reviewer. He's returned me few comments and there is one comment I'm not being able to solve.
The comment is the following:
Check if (query === this.state.query) to ensure you are not going to replace the contents to an old response
And part of the code is the one below:
updateQuery = (query) => {
this.setState({
query: query
})
this.updateWantedBooks(query);
}
updateWantedBooks = (query) => {
if (query) {
BooksAPI.search(query).then((wantedBooks) => {
if (wantedBooks.error) {
this.setState({ wantedBooks: [] });
} else {
this.setState({ wantedBooks: wantedBooks });
}
})
} else {
this.setState({ wantedBooks: [] });
}
}
Anyone could help me what do am I suppose to do?
Regards.
Code reviewer is right, you don't really want to replace the response if user has entered the very same query.
You have to store somewhere what for user has searched recently:
this.setState({ wantedBooks: [], query });
In case of success response:
this.setState({ wantedBooks, query });
And then check it in case of further searches:
if (query && query !== this.state.query) {
// continue the search only if query is different that current
Instead of relying on an outer member which is open to abuse by other code, you can employ a factory function to more safely trap a member.
As you have discovered, trapping and testing query == this.state.query can be made to work but is arguably not the best solution available.
With a little thought, you can force each call of updateWantedBooks() automatically to reject the previous promise returned by the same function (if it has not already settled), such that any success callbacks chained to the previous promise don't fire its error path is taken.
This can be achieved with a reusable canceller utility that accepts two callbacks and exploits Promise.race(), as follows:
// reusable cancellation factory utility
function canceller(work, successCallback) {
var cancel;
return async function(...args) {
if (cancel) {
cancel(new Error('cancelled')); // cancel previous
}
return Promise.race([
work(...args),
new Promise((_, reject) => { cancel = reject }) // rejectable promise
]).then(successCallback);
};
};
Here's a demo ...
// reusable cancellation factory utility
function canceller(work, successCallback) {
var cancel;
return async function(...args) {
if (cancel) {
cancel(new Error('cancelled')); // cancel previous
}
return Promise.race([
work(...args),
new Promise((_, reject) => { cancel = reject })
]).then(successCallback);
};
};
// delay utility representing an asynchronous process
function delay(ms, val) {
return new Promise(resolve => {
setTimeout(resolve, ms, val);
});
};
function MySpace() {
// establish a canceller method with two callbacks
this.updateWantedBooks = canceller(
// work callback
async (query) => delay(500, query || { 'error': true }), // a contrived piece of work standing in for BooksAPI.search()
// success callback
(wantedBooks => this.setState(wantedBooks)) // this will execute only if work() wins the race against cancellation
);
this.setState = function(val) {
console.log('setState', val);
return val;
};
};
var mySpace = new MySpace();
mySpace.updateWantedBooks({'value':'XXX'}).then(result1 => { console.log('result', result1) }).catch(error => { console.log(error.message) }); // 'cancelled'
mySpace.updateWantedBooks(null).then(result2 => { console.log('result', result2) }).catch(error => { console.log(error.message) }); // 'cancelled'
mySpace.updateWantedBooks({'value':'ZZZ'}).then(result3 => { console.log('result', result3) }).catch(error => { console.log(error.message) }); // {'value':'ZZZ'} (unless something unexpected happened)
Note that canceller() doesn't attempt to abort the asynchronous process it initiates, rather it stymies the success path of the returned promise in favour of the error path.
I think reviewer's point is that response of Search API is asynchronous and result for "query 1" can arrive after user changed his mind and already requested search "query 2". So when response arrive - we need to check if we really interested in it:
updateQuery = query => {
this.setState({
query: query
wantedBooks: []
})
this.updateWantedBooks(query);
}
updateWantedBooks = query => {
if (query) {
BooksAPI.search(query).then((wantedBooks) => {
// if updateQuery("query1) and updateQuery("query2") called in a row
// then response for query1 can arrive after we requested query2
// => for some period of time we'll show incorrect search results
// so adding check if query still the same can help
if (query !== this.state.query) {
// outdated response
return;
} else if (wantedBooks.error) {
// query is okay, but server error in response
this.setState({
wantedBooks: []
})
} else {
// success response to requested query
this.setState({ wantedBooks });
}
})
}
}
Guys I´ve done some tests with your answers, but I realize that somehow the code was behavioring strangely.
So, I've seen in other part of the reviewer comments, a part which I hadn't had seen before do my answer here, the following comment:
Inside 'then' part of the promise check if(query === this.state.query) to ensure you are not going to replace the contents to an old response.
And this "Inside 'then'" has been beating in my brain.
So, I think I've arrived in a satisfatory code; sure, maybe it isn't the definite solution, that's why I want to show here for you and feel free to comment if I'd have to make some improvement. Here below I put the code:
updateQuery = (query) => {
this.setState({
query: query
})
this.updateWantedBooks(query);
}
updateWantedBooks = (query) => {
if (query) {
BooksAPI.search(query).then((wantedBooks) => {
if (wantedBooks.error) {
this.setState({ wantedBooks: [] });
} else if (query !== this.state.query) {
this.setState( { wantedBooks: [] });
} else {
this.setState({ wantedBooks: wantedBooks });
}
})
} else {
this.setState({ wantedBooks: [] });
}
}
Regards

How can I run promises in sequence and dynamically

In my web application I can run blocks of code (it creates a promise and waits for the result to come out). Every time a user runs a paragraph, I add it's id to an array and run it sequentially.
runSequentially(paragraphsId) {
paragraphsId.reduce((promise, paragraphId) => {
return promise.then(() => this.runParagraph(paragraphId))
}, Promise.resolve())
}
addToQueue(paragraphId) {
if (this.state.runQueue.indexOf(paragraphId) === -1) {
this.setState({
runQueue: [...this.state.runQueue, paragraphId]
}, () => this.runSequentially(this.state.runQueue))
}
}
runParagraph(paragraphId) {
const newParagraphResults = { ...this.state.paragraphResults }
delete newParagraphResults[paragraphId]
const newParagraphs = { ...this.state.paragraphs }
const newParagraph = newParagraphs[paragraphId]
newParagraph.isRunning = true
newParagraph.status = 'running'
this.setState({
paragraphs: newParagraphs,
paragraphResults: newParagraphResults
})
const paragraphs = [
{
identifiers: { id: paragraphId },
title: newParagraph.title,
source: newParagraph.source
}
]
const notebookLibraries = Object.values(this.state.notebookLibraries)
this.runController = new AbortController()
return this.service.notebookRun(this.notebookId, paragraphs, notebookLibraries, this.runController)
.then(result => {
Object.entries(result.paragraphs).forEach(entry => {
if (entry[0] === 'default_paragraph') {
return
}
const paragraphId = entry[0]
const paragraphResult = entry[1]
newParagraphResults[paragraphId] = paragraphResult
paragraphResult.exception ? this.setParagraph(paragraphId, { status: 'failed' }) :
this.setParagraph(paragraphId, { status: 'passed' })
})
this.setState({ paragraphResults: newParagraphResults })
})
.catch((error) => {
if (error.name === 'AbortError') {
return Promise.reject(error)
}
const message = `Execution failed for reason: ${error.reason}.`
this.handleServiceError('notebook', 'run', message)
})
.finally(() => {
const newRunQueue = [ ...this.state.runQueue ]
newRunQueue.shift()
this.setParagraph(paragraphId, { isRunning: false })
this.setState({ runQueue: newRunQueue })
})
}
When a user runs a paragraph we call addToQueue which then calls runSequentially. We shift the queue when a promise is settled (in the runParagraph method) but if we run another paragraph before the first one has finished this will iterate over the same promise twice.
How would you handle this dynamic queue of promises ? Could recursivity work in this case ?
You should initialize another property (perhaps queue is not the best name since you already have state.runQueue) in your class to Promise.resolve(), and let that be the pending promise of your sequential queue. Then you can do something like this:
runSequentially(...paragraphsId) {
this.queue = paragraphsId.reduce((promise, paragraphId) => {
return promise.then(() => this.runParagraph(paragraphId))
}, this.queue)
}
addToQueue(paragraphId) {
if (this.state.runQueue.indexOf(paragraphId) === -1) {
this.setState({
runQueue: [...this.state.runQueue, paragraphId]
}, () => this.runSequentially(paragraphId))
}
}
runSequentially() now accepts incremental updates rather than the entire runQueue, and you shouldn't store queue on your state variable because the promise itself doesn't affect your render, so it's safe.
if we run another paragraph before the first one has finished this will iterate over the same promise twice.
You will need to keep the promise queue as a property in your state, instead of creating a new Promise.resolve() every time you call runSequentially. See here for an example implementation.
If you want to manage your queue strictly through setState, you should not need a runSequentially method at all. runParagraph itself would a) check whether it already is running and b) when finished dequeue the next element from the array and call runParagraph again until there a none left.

Array of 3 request functions - how to invoke the second function only if first function's response contains string? - Node JS

I have an array of 3 functions that use node-fetch to fetch data from 3 different APIs. I would like to only invoke the second and third function if the first function's response.body contains 'rejected'.
The problem I am running in to is that all the methods are being invoked before the response is received from the first.
const buyersList = [
{ buyerName: 'ACME',
buyerPrice: '100',
buyerMethod: sellACME,
},
{ buyerName: 'ACME',
buyerPrice: '60',
buyerMethod: sellACME,
},
{ buyerName: 'ACME',
buyerPrice: '20',
buyerMethod: sellACME,
},
{ buyerName: 'ACME',
buyerPrice: '2',
buyerMethod: sellACME,
},
];
//fetch the data and parse
function sellACME(url) {
return fetch(url, { method: 'POST' })
.then(obj => parseResponse(obj))
.catch(err => console.log(' error', err));
}
//shift the first item in array and execute the method for first item.
const methods = {};
methods.exBuyerSell = (url) => {
const currBuyer = buyersList.shift();
return currBuyer.buyerMethod(url);
};
//loop through array.
module.exports = (url, res) => {
while (buyersList.length > 0) {
methods.exBuyerSell(url)
.then((buyerRes) => {
//if response.result is accepted, empty array, send response to client.
if (buyerRes.result === 'accepted') {
buyersList.length = 0;
res.json(buyerRes);
}
//if response.result is rejected, execute the next item in array.
if (buyerRes.result === 'rejected') {
methods.exBuyerSell(url);
}
return buyerRes;
});
}
};
The business logic here is that, if the data is transmitted to the first buyer, the data is accepted by that buyer and can not be presented to the second buyer.
setTimeout() is not an option as the array can grow to be as long as 20 and each request can take up to 180 seconds.
I attempted to use async/await and async.waterfall, but still seemed to have the same issue.
This concept can be applied to your own use case. It will iterate over the collection until the promise resolves:
pipePromises([2,4,6,1], calculateOddity).then(res => console.log(res))
function pipePromises(ary, promiser) {
return ary.slice(1, ary.size).reduce(
(promise, n) => promise.catch(_ => promiser(n)),
promiser(ary[0])
);
}
function calculateOddity(n) {
return new Promise((resolve, reject) => {
n % 2 === 0 ? reject(n) : resolve(n);
})
}

Redux Thunk handling network responses in a different order

I have a text entry field which (via a Thunk) queries the server of the validity of the user's text. As these requests can happen in very quick succession, they may be returned from the server in a different order than when they were sent. Therefor, the string in the text field may be shown as invalid, when in fact it is valid.
To fix this, I'm performing a check when I receive a response from the server - is the current content of the text field the same as what was checked? If not, check again. I feel there should be a better way to handle this situation than querying a DOM element value.
How can I follow through from pre to post server request?
export function updateUserCode(code) {
return dispatch => {
return dispatch(validateUserCode(code))
}
}
function validateUserCode(code) {
return dispatch => {
dispatch(updateCode(code))
return fetch(`/api/code/${code}`)
.then(response => response.json())
.then(json => dispatch(receiveValidatedCode(code, json)))
.catch(error => {Log.error(error)})
}
}
function receiveValidatedCode(code, data) {
const lastval = document.getElementById('usercode').value
if (code != lastval) {
// Code not the same as current value
// need to validate again
return updateUserCode(lastval)
}
if(data.success) {
return {
type: types.CODE_VALIDATED,
code: code,
}
}
return {
type: types.CODE_INVALID,
reason: data.reason,
}
}
Messing with DOM inside your logic is indeed less than ideal. I would suggest keeping last entered text field value in Redux store and perform your checks in reducer.
Also I don't see any point in re-validating user input if current entered value differs from one validated by last resolved request. Just ignore such responses and do not perform unnecessary request.
In terms of code you can do it like that:
// actions
const requestValidation = value => ({ type: 'request-validation', value });
const receiveValidation = (value, result) => ({ type: 'receive-validation', value, result });
export const validateUserCode = code => dispatch => {
dispatch(requestValidation(code));
return fetch(`/api/code/${code}`)
.then(response => response.json())
.then(json => dispatch(receiveValidation(code, json)))
.catch(error => {Log.error(error)})
}
// reducer
export const validationReducer = (state = {}, action) => {
if (action.type === 'request-validation')
return ({ value: action.value, isValidating: true });
if (action.type === 'receive-validation' && state.value === action.value)
return ({ value: action.value, isValid: !!action.success });
return state;
};
That is not production quality code, and I don't event sure if it works, but it reflects the idea.

Categories

Resources