Skip to content

fix: replace sequential await-in-loop with Promise.all for concurrent execution#10480

Open
soccerthomas wants to merge 7 commits into
wso2:masterfrom
soccerthomas:fix-issue-27874
Open

fix: replace sequential await-in-loop with Promise.all for concurrent execution#10480
soccerthomas wants to merge 7 commits into
wso2:masterfrom
soccerthomas:fix-issue-27874

Conversation

@soccerthomas

Copy link
Copy Markdown

Purpose

Replace sequential await calls inside for...of loops with Promise.all in 5 files where the async calls are independent. Each loop was running API calls one at a time — this change lets them run concurrently, reducing total wait time from the sum of all calls to the duration of the slowest single call.

Files changed:

  • features/admin.administrators.v1/wizard/add-administrator-wizard.tsx
  • features/admin.connections.v1/components/edit/settings/authenticator-settings.tsx
  • features/admin.connections.v1/components/edit/settings/outbound-provisioning-settings.tsx
  • features/admin.console-settings.v1/hooks/use-enterprise-login-config.ts
  • features/admin.server-configurations.v1/pages/connector-listing-page.tsx

The remaining 9 locations flagged by the async-await-in-loop lint rule were left unchanged — they are while loops, already parallelized, or intentionally sequential.

Related Issues

Related PRs

  • N/A

Checklist

  • e2e cypress tests locally verified. (for internal contributers)
  • Manual test round performed and verified.
  • UX/UI review done on the final implementation.
  • Documentation provided.
  • Relevant backend changes deployed and verified
  • Unit tests provided.
  • Integration tests provided.

Security checks

Developer Checklist (Mandatory)

  • Complete the Developer Checklist in the related product-is issue to track any behavioral change or migration impact.

@CLAassistant

CLAassistant commented Jun 29, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Five internal async helpers are refactored from sequential loops to concurrent Promise.all execution, with a few local return types made explicit. The observable behavior within each helper remains otherwise the same.

Changes

Sequential-to-concurrent async refactor

Layer / File(s) Summary
Concurrent authenticator and connector fetching
features/admin.connections.v1/components/edit/settings/authenticator-settings.tsx, features/admin.connections.v1/components/edit/settings/outbound-provisioning-settings.tsx
fetchAuthenticators and fetchConnectors replace sequential loops with Promise.all over mapped async calls; fetchConnectors also gains an explicit Promise<OutboundProvisioningConnectorWithMetaInterface[]> return type.
Concurrent IDP group removal in removeConfiguration
features/admin.console-settings.v1/hooks/use-enterprise-login-config.ts
Removes the intermediate patchPromises array and inlines an async map wrapped by await Promise.all; per-role SCIM PATCH remove logic and error swallowing are unchanged.
Concurrent dynamic category loading in resolveDynamicCategories
features/admin.server-configurations.v1/pages/connector-listing-page.tsx
Filters non-predefined categories up-front, loads each via loadCategoryConnectors concurrently with Promise.all, applies SIFT filtering per result, and compacts with filter(Boolean).
Concurrent role updates and return type annotations in AddAdministratorWizard
features/admin.administrators.v1/wizard/add-administrator-wizard.tsx
assignUserRoles changed from sequential for...of to Promise.all; setIsSubmitting adapted per-task. assignAdminRole, sendExternalInvitation, and sendInternalInvitation receive explicit void return types. Trailing semicolon added at component closing brace.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Changeset Required ❌ Error PR changes only feature files; no .changeset/*.md file appears in the changed-file set, so the required changeset is missing. Add a new .changeset/*.md file listing the affected packages and version bumps (include @wso2is/console, @wso2is/myaccount, or @wso2is/identity-apps-core as applicable).
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses the five relevant async-await-in-loop violations with Promise.all and explains the remaining findings as non-applicable or already parallelized.
Out of Scope Changes check ✅ Passed The extra refactors stay within the same concurrency work and minor cleanup, with no unrelated feature additions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly matches the main change: replacing sequential awaits with Promise.all for concurrent execution.
Description check ✅ Passed The description follows the template and includes purpose, related issue, PRs, checklist, security checks, and developer checklist.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • 🛠️ create changeset

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@soccerthomas

Copy link
Copy Markdown
Author

Hi — this is my first contribution to this project. I've addressed the async-await-in-loop violations flagged in wso2/product-is#27874. Fixed 5 real violations with Promise.all and documented why the remaining 9 flagged locations were left unchanged. Would appreciate a review when you have time!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
features/admin.administrators.v1/wizard/add-administrator-wizard.tsx (1)

276-330: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Success alert and wizard close fire even when role updates fail.

Each task's .catch handles the error without re-throwing, so Promise.all always resolves. As a result, the success alert at Line 319 plus closeWizard()/onUserUpdate() run unconditionally, and any error set via setAlert(...) is immediately discarded because the wizard closes. Under concurrent execution this is more pronounced: multiple failing tasks overwrite each other's setAlert, and the user ends up seeing a success despite partial/total failure.

Separately, setIsSubmitting(false) inside the per-task .finally clears the loading state as soon as the first task settles, while other requests and the success path are still pending. Consider tracking failures and resetting submit state once after Promise.all.

♻️ Suggested handling
             setIsSubmitting(true);
-            await Promise.all(roleIds.map(async (roleId: string): Promise<void> => {
-                await updateUsersForRoleFunction(roleId, roleData)
-                    .catch((error: AxiosError<HttpErrorResponseDataInterface>) => {
+            let hasError: boolean = false;
+
+            await Promise.all(roleIds.map(async (roleId: string): Promise<void> => {
+                await updateUsersForRoleFunction(roleId, roleData)
+                    .catch((error: AxiosError<HttpErrorResponseDataInterface>) => {
+                        hasError = true;
                         if (!error.response || error.response.status === 401) {
                             setAlert({
                                 description: t(
                                     "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.description"
                                 ),
                                 level: AlertLevels.ERROR,
                                 message: t(
                                     "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.message"
                                 )
                             });
                         } else if (error.response && error.response.data && error.response.data.detail) {
                             setAlert({
                                 description: t(
                                     "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.description",
                                     { description: error.response.data.detail }
                                 ),
                                 level: AlertLevels.ERROR,
                                 message: t(
                                     "extensions:manage.users.wizard.addAdmin.internal.updateRole.error.message"
                                 )
                             });
                         } else {
                             // Generic error message
                             setAlert({
                                 description: t(
                                     "extensions:manage.users.wizard.addAdmin.internal.updateRole.genericError" +
                                     ".description"
                                 ),
                                 level: AlertLevels.ERROR,
                                 message: t(
                                     "extensions:manage.users.wizard.addAdmin.internal.updateRole.genericError.message"
                                 )
                             });
                         }
-                    })
-                    .finally(() => {
-                        setIsSubmitting(false);
-                    });
+                    });
             }));
+            setIsSubmitting(false);
+
+            if (hasError) {
+                return;
+            }
+
             dispatch(addAlert({
                 description: t(
                     "extensions:manage.users.wizard.addAdmin.internal.updateRole.success.description"
                 ),
                 level: AlertLevels.SUCCESS,
                 message: t(
                     "extensions:manage.users.wizard.addAdmin.internal.updateRole.success.message"
                 )
             }));
             closeWizard();
             onUserUpdate();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@features/admin.administrators.v1/wizard/add-administrator-wizard.tsx` around
lines 276 - 330, The role update flow in add-administrator-wizard.tsx is
treating failed updates as success because the per-role catch in the Promise.all
chain swallows errors, so the success alert, closeWizard(), and onUserUpdate()
run unconditionally. Update the submit handler around updateUsersForRoleFunction
so failures are tracked or re-thrown, only dispatch the success alert and close
the wizard after all role updates succeed, and move setIsSubmitting(false) to a
single place after the overall Promise.all completes or fails rather than in
each task’s finally.
🧹 Nitpick comments (1)
features/admin.administrators.v1/wizard/add-administrator-wizard.tsx (1)

237-237: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: annotate the variable type alongside the return type.

assignUserRoles now has the Promise<void> return type, but the const binding still relies on inference. Per coding guidelines, async functions should carry the Promise<T> annotation on both the variable and the function signature.

♻️ Suggested annotation
-    const assignUserRoles = async (users: UserBasicInterface[], roles: RolesInterface[]): Promise<void> => {
+    const assignUserRoles: (users: UserBasicInterface[], roles: RolesInterface[]) => Promise<void> =
+        async (users: UserBasicInterface[], roles: RolesInterface[]): Promise<void> => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@features/admin.administrators.v1/wizard/add-administrator-wizard.tsx` at line
237, The async helper assignUserRoles should explicitly annotate both the const
binding and the function signature with Promise<void> instead of relying on
inference. Update the declaration of assignUserRoles in
add-administrator-wizard.tsx so the variable itself is typed as an async
function returning Promise<void>, matching the existing return type annotation
on the function.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@features/admin.connections.v1/components/edit/settings/authenticator-settings.tsx`:
- Around line 438-446: The fetchAuthenticators helper is using Promise.all over
fetchAuthenticator, but a handled API failure can leave that promise pending and
block the pane loading state. Update fetchAuthenticator so it always settles on
every error path, or have it return null/undefined on failure and filter those
results in fetchAuthenticators, ensuring the effect that updates
isFederatedAuthenticatorFetchRequestLoading can complete even when one
authenticator fetch fails.

In
`@features/admin.connections.v1/components/edit/settings/outbound-provisioning-settings.tsx`:
- Around line 235-243: `fetchConnector()` in `OutboundProvisioningSettings` must
always settle with an `OutboundProvisioningConnectorWithMetaInterface` result or
a handled fallback, because the current error branches only dispatch alerts and
can leave `fetchConnectors()`/`Promise.all(...)` waiting indefinitely. Update
the error handling inside `fetchConnector()` so every failure path returns a
resolved value or explicitly rejects after alerting, and make sure the caller in
`fetchConnectors()` still aggregates the connector list correctly.

In `@features/admin.server-configurations.v1/pages/connector-listing-page.tsx`:
- Around line 284-301: Move the initial connector selection logic out of the
parallel category loading path in connector-listing-page.tsx:
loadCategoryConnectors() should only fetch and return category data, not update
selectedConnector. Compute the default selection after Promise.all completes,
using the deterministic category order and the first eligible connector, so the
choice does not depend on response timing or multiple concurrent
!selectedConnector checks.

---

Outside diff comments:
In `@features/admin.administrators.v1/wizard/add-administrator-wizard.tsx`:
- Around line 276-330: The role update flow in add-administrator-wizard.tsx is
treating failed updates as success because the per-role catch in the Promise.all
chain swallows errors, so the success alert, closeWizard(), and onUserUpdate()
run unconditionally. Update the submit handler around updateUsersForRoleFunction
so failures are tracked or re-thrown, only dispatch the success alert and close
the wizard after all role updates succeed, and move setIsSubmitting(false) to a
single place after the overall Promise.all completes or fails rather than in
each task’s finally.

---

Nitpick comments:
In `@features/admin.administrators.v1/wizard/add-administrator-wizard.tsx`:
- Line 237: The async helper assignUserRoles should explicitly annotate both the
const binding and the function signature with Promise<void> instead of relying
on inference. Update the declaration of assignUserRoles in
add-administrator-wizard.tsx so the variable itself is typed as an async
function returning Promise<void>, matching the existing return type annotation
on the function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 1a7b73c3-cca0-4797-a1c1-e2fadc5e3eea

📥 Commits

Reviewing files that changed from the base of the PR and between 0651514 and e959cc4.

📒 Files selected for processing (5)
  • features/admin.administrators.v1/wizard/add-administrator-wizard.tsx
  • features/admin.connections.v1/components/edit/settings/authenticator-settings.tsx
  • features/admin.connections.v1/components/edit/settings/outbound-provisioning-settings.tsx
  • features/admin.console-settings.v1/hooks/use-enterprise-login-config.ts
  • features/admin.server-configurations.v1/pages/connector-listing-page.tsx

Comment thread features/admin.connections.v1/components/edit/settings/authenticator-settings.tsx Outdated
Comment on lines +284 to +301
const dynamicCategories: GovernanceConnectorCategoryInterface[] = await Promise.all(
categories
.filter((category: GovernanceConnectorCategoryInterface) =>
!serverConfigurationConfig.predefinedConnectorCategories.includes(category.id))
.map(async (category: GovernanceConnectorCategoryInterface):
Promise<GovernanceConnectorCategoryInterface | null> => {
const connectorCategory: GovernanceConnectorCategoryInterface | null =
await loadCategoryConnectors(category.id);

// Filter out the SIFT connector from sub-organizations.
if (isSubOrganization() && connectorCategory?.connectors.length > 0) {
connectorCategory.connectors =
connectorCategory?.connectors?.filter((connector: GovernanceConnectorInterface) =>
connector.id !== ServerConfigurationsConstants.SIFT_CONNECTOR_ID);
}

// Filter out the SIFT connector from sub-organizations.
if (isSubOrganization() && connectorCategory?.connectors.length > 0) {
connectorCategory.connectors =
connectorCategory?.connectors?.filter((connector: GovernanceConnectorInterface) =>
connector.id !== ServerConfigurationsConstants.SIFT_CONNECTOR_ID);
}
connectorCategory && dynamicConnectorCategoryArray.push(connectorCategory);
}
}
return connectorCategory;
})

@coderabbitai coderabbitai Bot Jun 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Move initial connector selection out of the parallel fetch path.

After this change, every loadCategoryConnectors() call starts with the same render-scoped selectedConnector value, so multiple requests can all pass the !selectedConnector check at Line 350 and race to update it. That makes the default selection depend on response timing instead of a stable category order.

Suggested fix
         const dynamicConnectorCategoryArray: GovernanceConnectorCategoryInterface[] =
             dynamicCategories.filter(Boolean);
+
+        if (!selectedConnector && dynamicConnectorCategoryArray[0]?.connectors?.[0]) {
+            setSelectorConnector(dynamicConnectorCategoryArray[0].connectors[0] as GovernanceConnectorWithRef);
+        }
// Then remove the `!selectedConnector && setSelectorConnector(...)` side effect
// from `loadCategoryConnectors()` so each Promise stays pure.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@features/admin.server-configurations.v1/pages/connector-listing-page.tsx`
around lines 284 - 301, Move the initial connector selection logic out of the
parallel category loading path in connector-listing-page.tsx:
loadCategoryConnectors() should only fetch and return category data, not update
selectedConnector. Compute the default selection after Promise.all completes,
using the deterministic category order and the first eligible connector, so the
choice does not depend on response timing or multiple concurrent
!selectedConnector checks.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The selectedConnector race condition is pre-existing behavior outside the scope of this fix. This PR only addresses the async-await-in-loop violations flagged in #27874.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soccerthomas Fair point on scope. I'll clarify one nuance for the record: strictly speaking, the race isn't entirely pre-existing — in the previous sequential for...of implementation, loadCategoryConnectors() calls resolved one at a time, so the !selectedConnector check always ended up selecting the first connector from the first category in the array (deterministic). With Promise.all, all calls fire concurrently, so whichever category's request resolves first now determines the default selection, which can vary between requests depending on network timing.

That said, I agree it's a narrow, low-impact UX side effect (default connector selection) rather than a functional break tied to the async-await-in-loop fix this PR targets. I won't insist on it being addressed here — happy to leave it as-is for this PR. If you'd like, I can open a follow-up issue to track making the initial selection deterministic (e.g., picking it from dynamicConnectorCategoryArray[0] after Promise.all resolves) so it doesn't get lost. Let me know if you'd like me to file that.


🧠 Learnings used
Learnt from: PasinduYeshan
Repo: wso2/identity-apps PR: 9589
File: features/admin.connections.v1/components/edit/settings/outbound-provisioning-settings.tsx:575-578
Timestamp: 2026-02-15T17:06:33.975Z
Learning: In the identity-apps repository, prefer using the data-componentid attribute for component identification in tests. The data-testid attribute is deprecated. Use data-componentid for new components, while existing components may continue using data-testid until they are updated.

Learnt from: ShanChathusanda93
Repo: wso2/identity-apps PR: 9619
File: features/admin.issuer-usage-scope.v1/pages/issuer-usage-scope-configuration.tsx:74-76
Timestamp: 2026-02-18T10:58:59.745Z
Learning: In WSO2 Identity Apps, for any feature that modifies server configurations via the /api/server/v1/configs/ endpoint (e.g., Issuer Usage Scope), perform permission checks using featureConfig?.server?.scopes?.update instead of creating dedicated scope configurations. Apply this guideline to all relevant TSX files under features that interact with server config endpoints.

Learnt from: VivekVinushanth
Repo: wso2/identity-apps PR: 9651
File: features/admin.cds.v1/components/profile-attribute.tsx:25-27
Timestamp: 2026-02-25T11:23:23.686Z
Learning: In TSX files across the repo, when implementing dynamic key-value inputs and there is no equivalent component in wso2is/form (e.g., dynamic values editor), it is acceptable to reuse the legacy DynamicField as a temporary solution until an Oxygen UI alternative is built. Avoid creating a custom react-final-form FieldArray unless absolutely necessary. This guidance applies to components like profile-attribute.tsx and similar dynamic-field scenarios.

Learnt from: Lakshan-Banneheke
Repo: wso2/identity-apps PR: 10203
File: features/admin.approval-workflows.v1/pages/approval-workflows.tsx:232-235
Timestamp: 2026-04-28T04:55:21.209Z
Learning: In wso2/identity-apps, FeatureLockedBanner `sx` props are intentionally allowed to include hardcoded pixel values (e.g., `marginTop: '-20px'`) when used consistently across the existing pages (such as approval-workflows, console-settings, remote-userstores). During code review, do not flag these specific hardcoded pixel values in `FeatureLockedBanner` `sx` as a violation of any guideline that would otherwise require referencing theme values.

Learnt from: Lakshan-Banneheke
Repo: wso2/identity-apps PR: 10215
File: features/admin.groups.v1/components/wizard/create-group-wizard.tsx:603-611
Timestamp: 2026-04-29T15:34:05.529Z
Learning: When reviewing code in wso2/identity-apps, do not flag `TierLimitReachErrorModal` usages of `actionLabel` and `header` as “unused” when these props are currently not consumed by the call site. Keep the props passed through for consistency with other call sites and to support future extensibility (the component is imported from `wso2is/admin.core.v1/components/modals`).

@pavinduLakshan

Copy link
Copy Markdown
Member

Hi — this is my first contribution to this project. I've addressed the async-await-in-loop violations flagged in wso2/product-is#27874. Fixed 5 real violations with Promise.all and documented why the remaining 9 flagged locations were left unchanged. Would appreciate a review when you have time!

Hi @soccerthomas, thanks for the contribution. Could you please have a look at the coderabbitai bot comments and resolve those?

@pavinduLakshan

Copy link
Copy Markdown
Member

And let's enhance the PR title too

@soccerthomas soccerthomas changed the title Fix issue 27874 fix: replace sequential await-in-loop with Promise.all for concurrent execution Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[React Doctor] async-await-in-loop: await inside a for…of loop runs the calls sequentially — for independent oper... (14 occurrences)

3 participants