From eae3f97a44ad0a93ad963b2581cea88edb2a74ac Mon Sep 17 00:00:00 2001 From: hiteshjambhale Date: Fri, 17 Jul 2026 11:17:58 +0530 Subject: [PATCH] fix: dedupe concurrent getNodeAjaxOptions requests via in-flight sharing getNodeAjaxOptions cached responses only after they resolved, so concurrent callers for the same URL (e.g. every column row's Data Type dropdown mounting at once) all saw an empty cache and each fired its own identical HTTP GET. Track in-flight requests in a module-level Map keyed by the resolved URL and query params. Concurrent callers now await the same underlying request; the first to resolve populates the cache as before, and the entry is cleaned up on settle so the map does not leak. Fixes #10155 Co-Authored-By: Claude Opus 4.8 --- web/pgadmin/browser/static/js/node_ajax.js | 38 +++++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/web/pgadmin/browser/static/js/node_ajax.js b/web/pgadmin/browser/static/js/node_ajax.js index 3d71b29f827..c5749fa0473 100644 --- a/web/pgadmin/browser/static/js/node_ajax.js +++ b/web/pgadmin/browser/static/js/node_ajax.js @@ -77,6 +77,13 @@ export function generateNodeUrl(treeNodeInfo, actionType, itemNodeData, withId, } +/* Tracks AJAX requests that are currently in flight, keyed by the resolved + * URL + query params. This lets concurrent callers asking for the same options + * (e.g. every row's Data Type dropdown mounting at once) share a single HTTP + * request instead of each firing their own before the cache is populated. + */ +const _inflightAjaxRequests = new Map(); + /* Get the nodes list as options required by select controls * The options are cached for performance reasons. */ @@ -114,15 +121,28 @@ export function getNodeAjaxOptions(url, nodeObj, treeNodeInfo, itemNodeData, par let data = cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel); if (_.isUndefined(data) || _.isNull(data)) { - api.get(fullUrl, { - params: otherParams.urlParams, - }).then((res)=>{ - data = res.data; - if(res.data.data) { - data = res.data.data; - } - otherParams.useCache && cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel, data); - resolve(transform(data)); + // Share a single in-flight request among all concurrent callers asking + // for the same URL + params, so we don't fire duplicate GETs before the + // first response lands and populates the cache. + let inflightKey = fullUrl + '#' + JSON.stringify(otherParams.urlParams || {}); + let request = _inflightAjaxRequests.get(inflightKey); + if (!request) { + request = api.get(fullUrl, { + params: otherParams.urlParams, + }).then((res)=>{ + let resData = res.data; + if(res.data.data) { + resData = res.data.data; + } + otherParams.useCache && cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel, resData); + return resData; + }).finally(()=>{ + _inflightAjaxRequests.delete(inflightKey); + }); + _inflightAjaxRequests.set(inflightKey, request); + } + request.then((resData)=>{ + resolve(transform(resData)); }).catch((err)=>{ reject(err instanceof Error ? err : Error('Something went wrong')); });