fix: replace all async foreach callbacks#10157
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
1 Ignored Deployment
|
|
@nicktrn is attempting to deploy a commit to the cal Team on Vercel. A member of the Team first needs to authorize it. |
|
Thank you for following the naming conventions! 🙏 |
📦 Next.js Bundle Analysis for @calcom/webThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
| ); | ||
|
|
||
| const promises = bookingRefsFiltered.map(async (bookingRef) => { | ||
| if (!bookingRef.uid) return; |
There was a problem hiding this comment.
early return is preferable
| }); | ||
| await Promise.all(promises); | ||
| } catch (error) { | ||
| // FIXME: error logging - non-Error type errors are currently discarded |
There was a problem hiding this comment.
probably just a way around ts complaints, Promise.all will always throw instanceof Error
| ) { | ||
| try { | ||
| //schedule job to call subscriber url at the end of meeting | ||
| // FIXME: in-process scheduling - job will vanish on server crash / restart |
There was a problem hiding this comment.
just for future reference
| appId?: string | null, | ||
| isReschedule?: boolean | ||
| ) { | ||
| try { |
| if ( | ||
| callee.type === "MemberExpression" && | ||
| callee.property.type === "Identifier" && | ||
| callee.property.name === "forEach" |
There was a problem hiding this comment.
There is no type check here, so other non-Array forEach method calls may also be flagged. Should not be an issue for this project.
| const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter((ref) => | ||
| credentialsMap.has(ref.type) | ||
| ); | ||
| const pCancellations = bookingRefsFiltered.map(async (bookingRef) => { |
There was a problem hiding this comment.
another async forEach callback
| steps.forEach(async (step) => { | ||
| if (step.action !== WorkflowActions.SMS_ATTENDEE && step.action !== WorkflowActions.WHATSAPP_ATTENDEE) { | ||
| const pSteps = steps.map(async (step) => { | ||
| if ( | ||
| step.action !== WorkflowActions.SMS_ATTENDEE && | ||
| step.action !== WorkflowActions.WHATSAPP_ATTENDEE | ||
| ) { | ||
| //as we do not have attendees phone number (user is notified about that when setting this action) | ||
| bookingsForReminders.forEach(async (booking) => { |
There was a problem hiding this comment.
nested async forEach, when you think you've seen it all
| await ctx.prisma.workflowsOnEventTypes.createMany({ | ||
| data: activeOnEventTypes.map((eventType) => ({ | ||
| workflowId: id, | ||
| eventTypeId: eventType.id, | ||
| })), |
There was a problem hiding this comment.
this should run a lot quicker - only one query
There was a problem hiding this comment.
Let's do it as a separate performance optimization in another PR.
-- Edit
Nevermind, I think it's needed to solve the problem correctly. So, let's keep it.
| const pChildren = activeOnEventTypes.map((eventType) => | ||
| ctx.prisma.workflowsOnEventTypes.createMany({ | ||
| data: eventType.children.map((chEventType) => ({ | ||
| workflowId: id, | ||
| eventTypeId: chEventType.id, | ||
| })), | ||
| }) | ||
| ); | ||
| await Promise.all(pChildren); |
| } else if (reminder.method === WorkflowMethods.WHATSAPP) { | ||
| deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId); |
There was a problem hiding this comment.
seems this was forgotten here, but similar calls found elsewhere in the file
There was a problem hiding this comment.
That's interesting. @CarinaWolli can you confirm that the flow was missing whatsapp here.
There was a problem hiding this comment.
@nicktrn it can be a separate PR though to fix it, so that this change doesn't block this PR.
There was a problem hiding this comment.
I think there have been quite a few changes here since the last merge, probably fixed now!
There was a problem hiding this comment.
Not fixed on main, will revert it for now.
There was a problem hiding this comment.
I was also thinking that maybe classes would make sense here. We can have classes for all these channels(Whatsapp, SMS, Email) and the code here would simply do channel.deleteReminder. This would avoid having these if elses everywhere. This code is just too much involved with what's going on.
But classes might increase complexity in some other way.
There was a problem hiding this comment.
That's interesting. @CarinaWolli can you confirm that the flow was missing whatsapp here.
Yes, definitely looks like this was missing here. Thanks for finding that 🙏
|
Came across even more instances of this in different forms. It's impossible to tell in many places if this is desired behaviour or not. Might make sense to be explicit by requiring the use of a wrapper function like this: type NoArgFunction = () => unknown;
const asyncWrap = async (fn: NoArgFunction) => fn();
export const sendToBackgroundAndNeverThrow = (fn: NoArgFunction | NoArgFunction[]): void => {
if (typeof fn === "function") {
Promise.allSettled([asyncWrap(fn)]);
} else {
Promise.allSettled(fn.map(asyncWrap));
}
};Having to call |
361d55f to
3f1ad68
Compare
|
Closing as this was rather premature, and now outdated. |
This is a great initiative @nicktrn. Want to pick it back up? /bounty 50 |
|
💎 $50 bounty created by keithwillcode |
| }); | ||
|
|
||
| steps.forEach(async (step) => { | ||
| const pSteps = steps.map(async (step) => { |
There was a problem hiding this comment.
| const pSteps = steps.map(async (step) => { | |
| const promiseSteps = steps.map(async (step) => { |
Let's be clear here.
|
@keithwillcode Sure thing, will try to resolve those merge conflicts first of all. It's been a while. There are some decisions to be made on how to proceed though:
An alternative could be to add a wrapper as suggested in #10157 (comment) for now, and include error logging. |
|
@hariombalhara thanks for the review pointers so far, will help to keep this focused. |
| deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId); | ||
| } | ||
| }); | ||
| await Promise.all(pDeleteReminders); |
There was a problem hiding this comment.
How do you think about wrapping the code itself in Promise.all vs storing the result in pDeleteReminders and then doing await.
I find the earlier approach easier to read.
There was a problem hiding this comment.
So this becomes,
await Promise.all(remindersToUpdate.map((reminder) => {
if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId)
}
})There was a problem hiding this comment.
Sure that works. For longer mapping functions I tend to prefer prior assignment, but this is rather short.
There was a problem hiding this comment.
Will actually revert the whole thing. One of those implicit async calls that could probably do better with Promise.allSettled() in another PR. Error handling should really be discussed.
| try { | ||
| let scheduledJobs = booking.scheduledJobs || []; | ||
| let scheduledJobs = booking.scheduledJobs || []; | ||
| if (!booking.scheduledJobs) return; |
There was a problem hiding this comment.
What's the point of fallback initialization with empty array if we are still early returning in that case.
|
3f1ad68 to
7d2c886
Compare
| }); | ||
| await Promise.all(pCancellations); | ||
| // FIXME: error-handling | ||
| await Promise.allSettled( |
There was a problem hiding this comment.
Think this one is better suited for allSettled. Failure to delete a meeting should probably not prevent reschedule.
Have to say I'm quite worried by this PR. Will definitely need to be followed up with proper error handling / retries, at the very least additional logging. A big issue is that those async calls are now back in function scope, i.e. any rejected promise will prevent normal return. The good thing is this should highlight previously hidden errors. Are there any plans for background tasks? Think I saw some mention of redis-backed solutions a while back. Could also consider Trigger.dev. |
This reverts commit 53dfbcf.
Agree with this approach 🙏🏼. @nicktrn make sure you claim this PR so you get the bounty pay out when it merges |
|
Ah yes, thanks @keithwillcode. Appreciate the gesture but really wasn't necessary! |
|
💡 @nicktrn submitted a pull request that claims the bounty. You can visit your org dashboard to reward. |
|
🎉🎈 @nicktrn has been awarded $50! 🎈🎊 |

What does this PR do?
forEach(asyncFn)=>await Promise.all(map(asyncFn))Fixes #10156
/claim #10157
Also fixes:
Type of change
Caveat
Since we wait for promises to resolve, this will add parent function execution time - hopefully not much. In any case, this is likely preferable to unpredictable results.
Todo
Mandatory Tasks
Checklist