diff --git a/web/pgadmin/browser/static/js/node_ajax.js b/web/pgadmin/browser/static/js/node_ajax.js index 3d71b29f827..8ba04f7469b 100644 --- a/web/pgadmin/browser/static/js/node_ajax.js +++ b/web/pgadmin/browser/static/js/node_ajax.js @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////// import _ from 'lodash'; -import getApiInstance from '../../../static/js/api_instance'; +import getApiInstance, {getInflight} from '../../../static/js/api_instance'; import {generate_url} from 'sources/browser/generate_url'; import pgAdmin from 'sources/pgadmin'; @@ -114,15 +114,19 @@ 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, { + // 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. Each caller still runs + // its own caching + transform on the shared response. + getInflight(api, fullUrl, { params: otherParams.urlParams, }).then((res)=>{ - data = res.data; + let resData = res.data; if(res.data.data) { - data = res.data.data; + resData = res.data.data; } - otherParams.useCache && cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel, data); - resolve(transform(data)); + otherParams.useCache && cacheNode.cache(nodeObj.type + '#' + url, treeNodeInfo, cacheLevel, resData); + resolve(transform(resData)); }).catch((err)=>{ reject(err instanceof Error ? err : Error('Something went wrong')); }); diff --git a/web/pgadmin/static/js/api_instance.js b/web/pgadmin/static/js/api_instance.js index fda59b2190e..24d051d64a9 100644 --- a/web/pgadmin/static/js/api_instance.js +++ b/web/pgadmin/static/js/api_instance.js @@ -23,6 +23,61 @@ export default function getApiInstance(headers={}) { }); } +/* Tracks GET requests that are currently in flight, keyed by the resolved URL + + * params. This lets concurrent callers asking for the exact same resource + * (e.g. every column row's Data Type dropdown mounting at once on a wide table) + * share a single HTTP request instead of each firing their own identical GET. + */ +const _inflightGetRequests = new Map(); + +/* Builds a key that is independent of object key insertion order, so that + * {a:1, b:2} and {b:2, a:1} (and nested variants) resolve to the same key. + * Params are expected to be plain JSON-like query values (primitives, plain + * objects, arrays); exotic inputs like Date/Map/cyclic objects are not valid + * GET query params and are out of scope here. + * Primitives are type-tagged so that values that share a JSON form but differ + * in type never collide, e.g. the number 1 vs the string "1", or an array hole + * ([undefined]) vs the empty array ([]). */ +function stableStringify(value) { + if (value === undefined) { + return 'undefined'; + } + if (value === null || typeof value !== 'object') { + return typeof value + ':' + JSON.stringify(value); + } + if (Array.isArray(value)) { + return '[' + value.map(stableStringify).join(',') + ']'; + } + return '{' + Object.keys(value).sort().map( + (key) => JSON.stringify(key) + ':' + stableStringify(value[key]) + ).join(',') + '}'; +} + +/* Like api.get(url, config), but shares a single in-flight request among all + * concurrent callers requesting the same url + params. The shared entry is + * removed once the request settles (success or failure), so it never leaks and + * later calls fetch fresh data. Each caller attaches its own then/catch, so + * response handling (transform, caching, etc.) stays per-caller. */ +export function getInflight(api, url, config={}) { + // Key on url + params + any per-request headers, so callers that differ only + // in headers (e.g. a different Authorization) never share a response. The + // instance-level headers set by getApiInstance() are the session-global CSRF + // token and Content-type, identical across concurrent callers, so they don't + // need to be in the key; anything caller-specific should be passed here via + // config.headers. + const key = url + + '#' + stableStringify(config.params ?? {}) + + '#' + stableStringify(config.headers ?? {}); + let request = _inflightGetRequests.get(key); + if (!request) { + request = api.get(url, config).finally(() => { + _inflightGetRequests.delete(key); + }); + _inflightGetRequests.set(key, request); + } + return request; +} + export function parseApiError(error, withData=false) { if (error.response) { // The request was made and the server responded with a status code