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.
Related
I have an epic which should proceed few actions of type VERIFY_INSURANCE_REQUEST in a row. Everything works good inside switchMap block (all items are proceeded as well) but only last one goes to map block, so I have only one successfully dispatched action instead of many.
function verifyInsuranceEpic(action$) {
return action$.pipe(
ofType(types.VERIFY_INSURANCE_REQUEST),
switchMap((action) => {
const { verifyInsuranceModel } = action;
const promise = InsuranceApi.verifyInsurance(verifyInsuranceModel).then(result => {
const returnResult = result && result.rejectReason === null;
const actionResponse = {
returnResult,
key: verifyInsuranceModel.key
}
return actionResponse;
})
return from(promise);
}),
map(result => {
return verifyInsuranceSuccess(result)
}),
catchError(error => of(verifyInsuranceFailure(error)))
);
}
Is there any way to make all responses go to map block?
As mentioned in comments, the solution is just change switchMap to concatMap.
I have these methods that do some fetching, and then once done, they set the state. But the render is called before the state is done and does not update.
The below seems to work on it's own, but takes a minute to finish.
//returns an promise with Array
getTopIDs(url) {
return fetch(url).then(blob => blob.json()).then(json => json)
}
// makes a URL fetchs JSON and return promise with single ID
getStory(id) {
let url = `https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`
return fetch(url).then(blob => blob.json()).then(json => json)
}
// call above methods, set state when done
componentDidMount() { //
let arr = []
let promise = new Promise((resolve, reject) => {
let data = this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => {
this.getStory(id).then(res => {
arr.push(res)
})
})
//resolve once all pushed to arr
resolve(arr)
})
})
// set state once array is completed
promise.then(res => {
return this.setState({data: arr})
})
}
Then in the render below it logs 'no', 'no' and stops. Trying it outside the return it logs 'no','yes'. Searching other posts for this I tried setting a boolean when done and using the state callback but those did not work (full disclosure: I don't really understand the setState callback option)
render() {
return (
<div>
{
this.state.data.length
? console.log('yes')
: console.log('no')
}
</div>)
}
I need render to handle this.state.data only when done. How can I do it?
Add fiddle: https://jsfiddle.net/drumgod/e2atysu3/6/
Your method this.getStory() is async but your handling of the array creation is sync inside your promise.
You need to either use async/await or only run your resolve(arr) after idArr.forEach() is for sure completed (which may be easier to do using Promise.all(idArr.map(...)) where the ... is returning the result from this.getStory()).
This is how you'll want to set your state inside getStory:
this.setState(prevState => ({
data: [...prevState.data, res]
}))
As mentioned in the comments, this would render the component for each data point in the forEach.
In order to avoid this issue, this is how componentDidMount() should be formatted:
componentDidMount() {
const arr = [];
this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => this.getStory(id).then(res => arr.push(res)));
this.setState(prevState => ({ data: [...prevState.data, arr] }))
})
}
This also lets you get rid of the promise.then call at the end.
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 )
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);
})
}
I have an array like this
let array =[ {message:'hello'}, {message:'http://about.com'}, {message:'http://facebook.com'}]
I want to loop through it and at each item, I make a request to server to get open graph data, then save the fetched data back to array. Expected result
array =[
{message:'hello'},
{message: {
url:'http://about.com', title:'about'
}
},
{message:{
url:'http://facebook.com', title:'facebook'
}
}
]
And I need to execute something else after the async fully complete. Below code is what I think it would be
let requests = array.map( (item) => {
return new Promise( (resolve) => {
if (item.message.is('link')) {
axios.get(getOpenGraphOfThisLink + item.message)
.then( result => {
item.message =result.data
// console.log(item.message)
// outputs were
//{url:'http://about.com', title:'about'}
//{url:'http://facebook.com', title:'facebook'}
resolve()
})
}
})
})
Promise.all(requests).then( (array) => {
// console.log (array)
// nothing output here
})
The promise.all() won't run. console.log(array) does not output anything.
I see three main issues with that code:
Critically, you're only sometimes resolving the promise you create in the map callback; if item.message.is('link') is false, you never do anything to resolve. Thus, the Promise.all promise will never resolve.
You're accepting array as an argument to your Promise.all then callback, but it won't be one (or rather, not a useful one).
You're not handling the possibility of a failure from the axios call.
And if we resolve #2 by pre-filtering the array, then there's a fourth issue that you're creating a promise when you already have one to work with.
Instead:
let array = /*...*/;
let requests = array.filter(item => item.message.is('link'))
.map(item => axios.get(getOpenGraphicOfThisLink + item.message)
.then(result => {
item.message = result.data;
return result;
})
);
Promise.all(requests).then(
() => {
// Handle success here, using `array`
},
error => {
// Handle error
}
);
Note how reusing the axios promise automatically propagates the error up the chain (because we don't provide a second callback to then or a catch callback).
Example (doesn't demonstrate errors from axios.get, but...):
// Apparently you add an "is" function to strings, so:
Object.defineProperty(String.prototype, "is", {
value(type) {
return type != "link" ? true : this.startsWith("http://");
}
});
// And something to stand in for axios.get
const axios = {
get(url) {
return new Promise(resolve => {
setTimeout(() => {
resolve({data: "Data for " + url});
}, 10);
});
}
};
// The code:
let array =[ {message:'hello'}, {message:'http://about.com'}, {message:'http://facebook.com'}]
let requests = array.filter(item => item.message.is('link'))
.map(item => axios.get(/*getOpenGraphicOfThisLink + */item.message)
.then(result => {
item.message = result.data;
return result;
})
);
Promise.all(requests).then(
() => {
// Handle success here, using `array`
console.log(array);
},
error => {
// Handle error
console.log(error);
}
);