Describe the bug
getNodeAjaxOptions() in web/pgadmin/browser/static/js/node_ajax.js — the shared helper behind every AJAX-backed dropdown in pgAdmin (Data Type, foreign keys, triggers, backup/restore options, cloud wizards, ~35 call sites) — caches responses, but only after they resolve:
let data = cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel);
if (_.isUndefined(data) || _.isNull(data)) {
api.get(fullUrl, {...}).then((res) => {
...
cacheNode.cache(..., data); // only written once the response comes back
});
}
There is no in-flight request deduplication. If several components call this for the same URL before the first response lands, every one of them sees an empty cache and fires its own HTTP GET. Each row's Data Type dropdown (column.ui.js, cacheLevel: 'table') does exactly this on mount.
This code path exists on master as-is — node_ajax.js isn't touched by #10146. This issue was found while testing #10146 ("fix: skip row virtualization for small data grids"); we haven't separately confirmed how many duplicate requests fire on plain master for the same table, so we're not claiming that PR changes the severity — only reporting what we observed.
To Reproduce
- Create a table with 150+ columns.
- Open DevTools → Network tab.
- Right-click the table → Properties → Columns tab.
- Observe ~150 concurrent
GET /browser/table/get_types/<gid>/<sid>/<did>/<scid>/ requests, all firing within the same ~50-100ms window — one per column row, all for the identical options list.
Expected behavior
A single get_types request should be shared by all concurrent callers; the rest should await that same in-flight request rather than issuing their own.
Additional context
x
Found while testing #10146 — noting that for traceability, not to imply it's the cause. The underlying race is in node_ajax.js, which that PR does not modify.
Proposed fix
Add in-flight promise sharing to getNodeAjaxOptions, keyed by the resolved URL + query params, so concurrent callers await the same underlying request:
const _inflightAjaxRequests = new Map();
...
if (_.isUndefined(data) || _.isNull(data)) {
const 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.data ?? res.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(...));
}
Describe the bug
getNodeAjaxOptions()inweb/pgadmin/browser/static/js/node_ajax.js— the shared helper behind every AJAX-backed dropdown in pgAdmin (Data Type, foreign keys, triggers, backup/restore options, cloud wizards, ~35 call sites) — caches responses, but only after they resolve:There is no in-flight request deduplication. If several components call this for the same URL before the first response lands, every one of them sees an empty cache and fires its own HTTP GET. Each row's Data Type dropdown (
column.ui.js,cacheLevel: 'table') does exactly this on mount.This code path exists on
masteras-is —node_ajax.jsisn't touched by #10146. This issue was found while testing #10146 ("fix: skip row virtualization for small data grids"); we haven't separately confirmed how many duplicate requests fire on plainmasterfor the same table, so we're not claiming that PR changes the severity — only reporting what we observed.To Reproduce
GET /browser/table/get_types/<gid>/<sid>/<did>/<scid>/requests, all firing within the same ~50-100ms window — one per column row, all for the identical options list.Expected behavior
A single
get_typesrequest should be shared by all concurrent callers; the rest should await that same in-flight request rather than issuing their own.Additional context
x
Found while testing #10146 — noting that for traceability, not to imply it's the cause. The underlying race is in
node_ajax.js, which that PR does not modify.Proposed fix
Add in-flight promise sharing to
getNodeAjaxOptions, keyed by the resolved URL + query params, so concurrent callers await the same underlying request: