Reduce on list of promises - javascript

I have a list of jobs that should be sequentially executed.
As the jobs take seconds to be finished the should run in the background.
I thought that a job could be described as
interface Job {
name: string
execute(): Promise<boolean>
}
I would like to have a function which takes this list of jobs and execute them sequentially
until the list is completed or one job fails or is rejected, so basically:
function executeUntilFailed(jobs: Job[]): Promise<boolean>
{
// execute first job
// if this job
// - returns with true: continue with the next job
// - returns with false: resolve the promise with false
// - got rejected: reject the promise with the reason prefixed with the jobs name
//
// if there are no more jobs to do, resolve the promise with true
//
// basically it's a reduce operation with starting value of true and
// early stops if one job yields false or got rejected
}
I'm rather new to Javascript/Typescript and have a hard time implementing this.
Thanks,
Dieter

Thanks to Aluan Hadded and ehab.
I collected their solutions and have now the following code,
which does exactly what I need:
interface Job {
name: string
execute(): Promise<boolean>
}
async function executeUntilFailed(jobs: Job[]) {
for (const job of jobs) {
try {
if(!await job.execute()) {
return false
}
}
catch (err) {
throw new Error(`${job.name}: ${err.message}`)
}
}
return true
}
and here is some example for it
class JobImpl implements Job {
constructor(public name: string, private value: boolean, private throwMsg: string|null = null) {}
execute(): Promise<boolean> {
console.log(`executing job '${this.name}'`)
return new Promise((resolve,reject) => {
setTimeout(()=> {
if(this.throwMsg!=null) { reject(this.throwMsg) }
else { console.log(`finished job '${this.name}' with result: ${this.value}`); resolve(this.value) }
}, 1000)
})
}
}
const successJobs = [
new JobImpl("a", true),
new JobImpl("b", true),
new JobImpl("c", true),
]
const failedJobs = [
new JobImpl("d", true),
new JobImpl("e", false),
new JobImpl("f", true),
]
const throwingJobs = [
new JobImpl("g", true),
new JobImpl("g", true, "undefined problem"),
new JobImpl("i", true),
]
executeUntilFailed(successJobs)
.then((res) => console.log("resolved", res))
.catch((err) => console.log("rejected", err))
executeUntilFailed(failedJobs)
.then((res) => console.log("resolved", res))
.catch((err) => console.log("rejected", err))
executeUntilFailed(throwingJobs)
.then((res) => console.log("resolved", res))
.catch((err) => console.log("rejected", err))
<!-- end snippet -->

Just as an alternative, you could create a generator and then use the for await ... of syntax:
function * chainJobs(jobs) {
for (const job of jobs) {
yield job.execute().catch(err => new Error(`${job.name}: ${err.message}`));
}
}
async function executeUntilFailed(jobs) {
for await (const result of chainJobs(jobs)) {
if (!result) return false;
if (result instanceof Error) throw result;
}
return true;
}

You could achieve this either with a reduce function or a for of loop, i will show an implementation in a for of
async function executeUntilFailed(jobs) {
for (const job of jobs) {
try {
// notice that if a job resolved with false then it is considered a successful job
// This is normal and a promise resolved with false should not be considered an error
await job.execute()
// if u want based on your description to resolve the whole promise with false if one of promises resolved with false you could do
// const jobResult = await job.execute()
// if (jobResult === false) {
// return Prmise.resolve(false)
// }
} catch (err) {
return Promise.reject(new Error(`${job.name}_${err.toString()}`))
}
}
return Promise.resolve(true)
}
lets see the function in action
const successJobs = [{
name: "a",
execute: () => Promise.resolve(1)
},
{
name: "b",
execute: () => Promise.resolve(2),
},
{
name: "c",
execute: () => Promise.resolve(3)
},
]
const failedJobs = [{
name: "a",
execute: () => Promise.resolve(1)
},
{
name: "b",
execute: () => Promise.reject(new Error("undefined problem")),
},
{
name: "c",
execute: () => Promise.resolve(3)
},
]
async function executeUntilFailed(jobs) {
for (const job of jobs) {
try {
await job.execute()
} catch (err) {
return Promise.reject(new Error(`${job.name}_${err.toString()}`))
}
}
return Promise.resolve(true)
}
console.log(
executeUntilFailed(successJobs)
.then((res) => console.log("resolved", res))
.catch((err) => console.log("rejected", err))
)
console.log(
executeUntilFailed(failedJobs)
.then((res) => console.log("resolved", res))
.catch((err) => console.log("rejected", err))
)

Related

Can not return from a function

I have a function that looks like following
export const checkForAvailableAgent = (topicId, serviceUrl, serviceId) => {
const serviceInfo = new window.adiaLive.ServiceInfo({
topicId: topicId, // set here the topicId which you want listen for
OnError: e => {
// react to error message (optional)
console.log("error: ", e);
},
OnServiceStateChange: e => {
if (e.ConnectedAdvisers > 0) {
// there are advisers online for given topicId
console.log("studio available");
return true;
} else {
console.log("studio not available");
return false;
}
}
});
serviceInfo.connect(serviceUrl, serviceId);
};
however the return statements don't return anything when I use the function in the following manner
useEffect(() => {
const agent = checkForAvailableAgent(
`sales_${i18n.language}`,
"https://linktoserviceurl",
"serviceid"
);
// console.log("studio available is: ", agent);
}, []);
the console.log massages appear but the return statement is undefined.
any help would be appreciated.
You can not return from a callback function, as it is running asynchronously and you are not waiting for it to have a result ready.
You can however make the function itself async by returning a Promise instead of the actual result and wait until the Promise has a result ready (e.g. it is resolved):
export const checkForAvailableAgent = (topicId, serviceUrl, serviceId) => {
return new Promise((resolve, reject) => {
const serviceInfo = new window.adiaLive.ServiceInfo({
topicId: topicId, // set here the topicId which you want listen for
OnError: e => {
// react to error message (optional)
console.log("error: ", e);
reject(); // reject on failure
},
OnServiceStateChange: e => {
if (e.ConnectedAdvisers > 0) {
// there are advisers online for given topicId
console.log("studio available");
resolve(true); // resolve instead of return
} else {
console.log("studio not available");
resolve(false);
}
}
});
serviceInfo.connect(serviceUrl, serviceId);
})
};
useEffect(() => {
checkForAvailableAgent(
`sales_${i18n.language}`,
"https://linktoserviceurl",
"serviceid"
).then((agent) => { // then callback is called when the promise resolved
console.log("studio available is: ", agent);
}).catch(error => { // catch is called when promise got rejected
console.log('An error happened');
});
}, []);
The function servceInfo.OnServiceStateChange is a function into the object (seems to be an event).
I'd suggest declaring a variable on the checkForAvailableAgent like connected and change it's value when the event is called.
Then access it using checkForAvailableAgent.connected.
A version with async/await and try/catch
export const checkForAvailableAgent = (topicId, serviceUrl, serviceId) => {
return new Promise((resolve, reject) => {
const serviceInfo = new window.adiaLive.ServiceInfo({
topicId: topicId,
OnError: reject,
OnServiceStateChange: e => resolve(e.ConnectedAdvisers > 0)
});
serviceInfo.connect(serviceUrl, serviceId);
})
};
useEffect(() => {
(async () => {
try {
const isAvailable = await checkForAvailableAgent(
`sales_${i18n.language}`,
"https://linktoserviceurl",
"serviceid"
);
// console.log("Result", isAvailable)
} catch(e) {
console.error(e)
}
})()
// console.log("studio available is: ", agent);
}, []);
There are 2 possible reasons
you are not returning anything from checkForAvailableAgent.
After returning from the checkForAvailableAgent, it might be asynchronous function. You can use async & await.

How to insert value of an array inside onSnapshot function from firebase which is also inside the loop

If I put the arr.push() outside the onSnapshot function that's the only time the array will return the value. But all I want is to insert the data within onSnapshot. Is anyone here can help me on this kind of problem?
inboxTodo.get().then(snap => {
let arr = [] //Initialize array.
snap.data().todos.forEach(todo => {
cars.doc(String(doc.data().id)).collection('todo').doc(String(todo))
.onSnapshot(t => {
const value = {
id: doc.data().id,
value: t.data()
}
arr.push(value) // insert some data inside the array.
this.props.setTodo(value)
}, err => {
alert(err)
})
}, err => {
alert(err)
})
console.log(arr) //returns an empty array :(
}, err => {
alert(err);
})
.onSnapshot takes a callback, which is asynchronous. So when your console.log executes, none of the .onSnapshot callbacks have been resolved, hence why your array is still empty.
You can know when all of the snapshot callbacks have fired by Promisifying them.
You'll need to do something like this: (untested)
inboxTodo.get().then(snap => {
let promiseArr = [] // make an array of promises to be executed
snap.data().todos.forEach(todo => {
// Promisify each .onSnapshot
promiseArr.push(new Promise(resolve => {
cars.doc(String(doc.data().id)).collection('todo').doc(String(todo))
.onSnapshot(t => {
const value = {
id: doc.data().id,
value: t.data()
}
resolve(value) // resolve the new promise
this.props.setTodo(value)
}, err => {
alert(err)
})
});
}, err => {
alert(err)
})
// exectue all promises
Promise.all(promiseArr)
.then(values => {
console.log(values) // all your array values will be here
})
}, err => {
alert(err);
})

Function to return non-null value not working properly when saving data to MongoDB

I get an array of objects from an API call and then I filter the values depending on two keys: story_title and title. If both values are null then the object is filtered. Then I loop through the filtered array of objects to save certain data to mongodb (using mongoose) from the filtered array. The problem is that I want to save the document with one title key, so I created a function to check if story_title or title is null and return the non-null value.
The function is not working properly because the function in title, inside the for loop, is returning some null values.
function pickTitle(title, story_title) {
if (!story_title) {
return story_title;
} else {
return title
}
}
everyHour: async () => {
try {
data = await axios.get(url);
let posts = data.data.hits;
const filteredPosts = await posts.filter((elem) => {
return (!elem.title || !elem.story_title)
});
for (let filtered of filteredPosts) {
Post.updateOne({
_id: filtered.objectID,
author: filtered.author,
title: await pickTitle(filtered.title, filtered.story_title),
created_at: filtered.created_at,
},
{$setOnInsert: filtered},
{upsert: true},
function(err, numAffected) {
if (err) {
//console.log("err")
} else {
//console.log(numAffected)
}
})
.then(res => {
//console.log(res)
})
.catch(err => {
//console.log(err)
});
}
} catch(error) {
console.log(error);
}
}
There are a few issues here. I'll step through them in some comments in the code, as well as having the "solution" directly below the commented code.
You are awaiting calls that are not asynchronous... don't do that. Array.filter is not an asynchronous operation
Your function pickTitle is being awaited, when it's not asynchronous
Your resolving promises inside a loop, which is generally considered bad practice. Add your promises to an array, and resolve everyone with Promise.all()
Lastly, your "filter" logic filters on NULL title OR story_title. That means only one needs to hold true. It's possible that both could be NULL though. Thus your pickTitle function returns a null value if both happen to be NULL. If you want to always have at least one of them contain a value, you need to change the way your array.filter works.
const pickTitle = (title, story_title) => {
if (!story_title) {
return story_title;
}
return title;
};
async () => {
try {
data = await axios.get(url);
const posts = data.data.hits;
// const filteredPosts = await posts.filter(elem => (!elem.title || !elem.story_title)); <-- you are awaiting on a filter... don't do that
const filteredPosts = posts.filter(elem => (!elem.title || !elem.story_title));
const filteredPostAnotherWay = posts.filter(post => { // <-- This might be more of what you want...
let allowed = false;
if (!post.title) {
allowed = true;
}
if (!post.story_title) {
allowed = true;
}
return allowed;
});
const postUpdates = [];
for (const filtered of filteredPosts) {
// Post.updateOne({ <-- You are resolving promises inside a for loop. While it may work, it's generally not advised to do this. Resolve everything with promise.all instead....
// _id: filtered.objectID,
// author: filtered.author,
// title: await pickTitle(filtered.title, filtered.story_title),
// created_at: filtered.created_at,
// story_url: filtered.story_url
// },
// { $setOnInsert: filtered },
// { upsert: true },
// (err, numAffected) => {
// if (err) {
// // console.log("err")
// } else {
// // console.log(numAffected)
// }
// })
// .then(res => {
// // console.log(res)
// })
// .catch(err => {
// // console.log(err)
// });
postUpdates.push(
Post.updateOne({
_id: filtered.objectID,
author: filtered.author,
// title: await pickTitle(filtered.title, filtered.story_title), // <-- You are awaiting a non asynchronous function... why?
title: pickTitle(filtered.title, filtered.story_title),
created_at: filtered.created_at,
story_url: filtered.story_url
},
{ $setOnInsert: filtered },
{ upsert: true },
(err, numAffected) => {
if (err) {
// console.log("err")
} else {
// console.log(numAffected)
}
})
);
}
return Promise.all(postUpdates);
} catch (error) {
console.log(error);
}
};

Promise.all response

Can I add Promise.resolve(value) and Promise.reject(error) at response of Promise.all().
For example,
function transferFRQ(fromUserId, fromCompanyDetails, toUserDetails, toCompanyDetails,) {
return Promise.all([
transferRFQCompany(fromCompanyDetails, toCompanyDetails),
replaceRFQCreatedBy(fromUserId, toUserDetails)
])
.then(result => Promise.resolve(result))
.catch(error => Promise.reject(error));
}
function transferRFQCompany (fromCompanyDetails, toCompanyDetails) {
return new Promise((resolve, reject) => {
Request.updateMany({
"company.id": fromCompanyDetails._id
}, {
$set: {
company: {
id: toCompanyDetails._id,
name: toCompanyDetails.name,
logo: toCompanyDetails.logo
}
}
}).then(result => resolve(result))
.catch(error => reject(error));
});
}
function replaceRFQCreatedBy (fromUserId, toUserDetails) {
return new Promise((resolve, reject) => {
Request.updateMany({
"createdBy.id": fromUserId
}, {
$set: {
createdBy: {
id: toUserDetails._id,
firstName: toUserDetails.firstMame,
lastName: toUserDetails.lastName
}
}
}).then(result => resolve(result))
.catch(error => reject(error));
});
}
I don't know whether is it correct or not, but what I need is to handle the response of transferRFQ properly because I will need to add transferRFQ in another Promise.all() to handle the error properly.
Am I doing in the wrong way? if so, How to do it correctly
Any additional advice is welcome!
I advice that you should not use unnecessary Promise wrappers, & avoid javascript hoisting.
You should try to do something like this instead
// Note that this is declared before used, to avoid javascript hoisting
function transferRFQCompany (fromCompanyDetails, toCompanyDetails) {
return Request.updateMany({ // updateMany already returns a promise right? no need to wrap it in another promise
"company.id": fromCompanyDetails._id
}, {
$set: {
company: {
id: toCompanyDetails._id,
name: toCompanyDetails.name,
logo: toCompanyDetails.logo
}
}
})
});
}
// Note that this is declared before used, to avoid javascript hoisting
function replaceRFQCreatedBy (fromUserId, toUserDetails) {
return Request.updateMany({ // updateMany already returns a promise right? no need to wrap it in another promise
"createdBy.id": fromUserId
}, {
$set: {
createdBy: {
id: toUserDetails._id,
firstName: toUserDetails.firstMame,
lastName: toUserDetails.lastName
}
}
})
}
function transferFRQ(fromUserId, fromCompanyDetails, toUserDetails, toCompanyDetails,) {
return Promise.all([
transferRFQCompany(fromCompanyDetails, toCompanyDetails),
replaceRFQCreatedBy(fromUserId, toUserDetails)
])
}
// Sample usage async/await style
(async () => {
try {
// put your params, of course
const result = await transferFRQ(...params);
// `result` is result of .then()
} catch (e) {
// `e` is result of .catch()
}
// or use it in promise-style
transferFRQ(...params)
.then(console.log)
.catch(console.error)
})()

Promise continues after error

I have some async code that needs to stop in case of error but keeps executing:
async saveCoupons({ state, rootState, dispatch, commit }) {
const promises = []
state.userCoupons.forEach(coupon => {
if (coupon.isNew && coupon.isUpdated) {
// if the user is creating a new coupon
promises.push(Vue.axios.post('/api_producer/coupons.json', coupon, { params: { token: coupon.token } }))
} else if (!coupon.isNew && coupon.isUpdated) {
// if the user is updating the coupon
promises.push(Vue.axios.patch(`api_producer/coupons/${coupon.id}/`, coupon, { params: { token: coupon.token } }))
}
})
try {
await Promise.all(promises)
dispatch('utilities/showModal', 'success', { root: true })
dispatch('fetchProducerCoupons')
} catch (err) {
let couponToken = err.request.responseURL.split('token=')[1]
commit('ADD_ERROR_ON_COUPON', couponToken)
console.log(err)
}
}
This is how the code is currently structured, it works, but I realize it's terrible. What I need to do is stop the excution of
dispatch('utilities/showModal', 'success', { root: true })
dispatch('fetchProducerCoupons')
In case one of the api calls fails. I wanted to catch the error inside the forEach so I already have the item available and I can add the error to it right away as opposed to doing it after (which is what I'm doing now with { params: { token: coupon.token } }.
I think the best way would be to wrap the Vue.axios requests into your own Promise. Then, if the requests fail, you have the coupon tokens in your error.
Something like
const promises = [];
promises.push(
Vue.axios.post('/api_producer/coupons.json', coupon)
.catch(() => { throw new Error(coupon.token) }));
Promise.all(promises).catch(tokens => {
tokens.forEach(token => {
// Commit/handle errorous token
});
});
You can wrap your api call in another promise and check the status. Something like this:
promises.push(
new Promise((resolve, reject) => {
Vue.axios.post('/api_producer/coupons.json', coupon, { params: { token: coupon.token } })
.then((response) => {
if (response.status !== 200) {
coupon.error = true;
reject();
} else {
resolve();
}
});
})
);
The reject will keep these two lines from being executed:
dispatch('utilities/showModal', 'success', { root: true })
dispatch('fetchProducerCoupons')
Thanks to Moritz Schmitz v. Hülst & sklingler93 for the help, I restructured the code and it's working.
I'm wondering if there's a way to write all of this using only async/await... If anybody has an idea, would love to see it :)
saveCoupons({ state, rootState, dispatch, commit }) {
const promises = []
state.userCoupons.forEach(coupon => {
if (coupon.isNew && coupon.isUpdated) {
// if the user is creating a new coupon
promises.push(new Promise((resolve, reject) => {
Vue.axios.post('/api_producer/coupons.json', coupon)
.then(response => resolve(response))
.catch(err => {
reject(err)
commit('ADD_ERROR_ON_COUPON', coupon.token)
})
}))
} else if (!coupon.isNew && coupon.isUpdated) {
// if the user is updating the coupon
promises.push(new Promise((resolve, reject) => {
Vue.axios.patch(`api_producer/coupons/${coupon.id}/`, coupon)
.then(response => resolve(response))
.catch(err => {
reject(err)
commit('ADD_ERROR_ON_COUPON', coupon.token)
})
}))
}
})
Promise.all(promises)
.then(() => {
dispatch('utilities/showModal', 'success', { root: true })
dispatch('fetchProducerCoupons')
})
.catch(err => console.error(err))
},

Categories