1
0
mirror of synced 2026-10-24 18:59:25 +03:00
Files
pgadmin4/web/regression/javascript/api_instance.spec.js
hiteshjambhale 104d018677 fix: dedupe concurrent getNodeAjaxOptions requests (#10307)
getNodeAjaxOptions() in web/pgadmin/browser/static/js/node_ajax.js, the
shared helper behind every AJAX-backed dropdown, cached responses only
once they had resolved, so when many components asked for the same URL
before the first response landed each of them saw an empty cache and
fired its own identical GET. Every column row's Data Type dropdown
mounting at once on a wide table's Columns tab therefore produced one
get_types request per row: around 150 concurrent duplicates on a
150-column table.

Concurrent GETs are now shared in the Axios wrapper. A module-level Map
tracks requests in flight, keyed on the request axios will actually make:
api.getUri() resolves the URI exactly as the request will, honouring the
instance baseURL, plain-object params, URLSearchParams and any custom
paramsSerializer, whilst the instance's own header configuration and the
per-request headers are hashed in alongside it, so an instance built with
extra headers never shares with a plain one. Params are canonicalised
first so that callers passing the same values in a different order still
share. The entry is removed once the request settles, success or failure,
so nothing leaks and later callers fetch fresh data.

Beyond params and headers there is plenty in an axios config that cannot
be captured in a key and that changes either what a caller gets back or
how it can be aborted: transformResponse, responseType, validateStatus,
timeout, signal and so on. Rather than let those share silently, a
request carrying anything outside the shareable set goes straight through
to api.get().

Each caller still runs its own caching and transform on the shared
response, so cached behaviour and response-shape handling are unchanged.

Fixes #10155.
2026-08-18 10:02:30 +01:00

153 lines
4.8 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import getApiInstance, {getInflight} from 'sources/api_instance';
describe('getInflight', ()=>{
let networkMock;
beforeEach(()=>{
networkMock = new MockAdapter(axios);
networkMock.onGet('/types/').reply(200, {data: [{label: 'text'}]});
networkMock.onGet('/broken/').reply(500, {errormsg: 'nope'});
});
afterEach(()=>{
networkMock.restore();
});
it('shares one request among concurrent callers', async ()=>{
// Every getNodeAjaxOptions() call builds its own api instance, so the
// shared request has to survive distinct-but-identically-configured ones.
const [res1, res2] = await Promise.all([
getInflight(getApiInstance(), '/types/', {params: {tid: 1}}),
getInflight(getApiInstance(), '/types/', {params: {tid: 1}}),
]);
expect(networkMock.history.get.length).toBe(1);
expect(res1.data).toEqual({data: [{label: 'text'}]});
expect(res2).toBe(res1);
});
it('ignores param order when matching', async ()=>{
await Promise.all([
getInflight(getApiInstance(), '/types/', {params: {tid: 1, scid: 2}}),
getInflight(getApiInstance(), '/types/', {params: {scid: 2, tid: 1}}),
]);
expect(networkMock.history.get.length).toBe(1);
});
it('does not share requests with different params', async ()=>{
await Promise.all([
getInflight(getApiInstance(), '/types/', {params: {tid: 1}}),
getInflight(getApiInstance(), '/types/', {params: {tid: 2}}),
]);
expect(networkMock.history.get.length).toBe(2);
});
it('does not share requests with different URLSearchParams', async ()=>{
await Promise.all([
getInflight(getApiInstance(), '/types/', {
params: new URLSearchParams([['tid', '1']]),
}),
getInflight(getApiInstance(), '/types/', {
params: new URLSearchParams([['tid', '2']]),
}),
]);
expect(networkMock.history.get.length).toBe(2);
expect(
networkMock.history.get.map((r)=>r.params.toString()).sort()
).toEqual(['tid=1', 'tid=2']);
});
it('does not share requests serialised differently', async ()=>{
await Promise.all([
getInflight(getApiInstance(), '/types/', {
params: {tid: 1},
paramsSerializer: {serialize: ()=>'tid=1'},
}),
getInflight(getApiInstance(), '/types/', {
params: {tid: 1},
paramsSerializer: {serialize: ()=>'tid=2'},
}),
]);
expect(networkMock.history.get.length).toBe(2);
});
it('does not share requests with different instance headers', async ()=>{
await Promise.all([
getInflight(getApiInstance(), '/types/', {params: {tid: 1}}),
getInflight(
getApiInstance({'Content-Encoding': 'gzip'}), '/types/',
{params: {tid: 1}}
),
]);
expect(networkMock.history.get.length).toBe(2);
});
it('does not share requests with different per-request headers', async ()=>{
await Promise.all([
getInflight(getApiInstance(), '/types/', {
params: {tid: 1}, headers: {'X-Test': 'a'},
}),
getInflight(getApiInstance(), '/types/', {
params: {tid: 1}, headers: {'X-Test': 'b'},
}),
]);
expect(networkMock.history.get.length).toBe(2);
});
it('never shares requests carrying unkeyed config', async ()=>{
// responseType changes what the caller gets back, so these two must not
// be handed the same response even though url and params match.
await Promise.all([
getInflight(getApiInstance(), '/types/', {
params: {tid: 1}, responseType: 'text',
}),
getInflight(getApiInstance(), '/types/', {
params: {tid: 1}, responseType: 'text',
}),
]);
expect(networkMock.history.get.length).toBe(2);
});
it('fetches again once the shared request has settled', async ()=>{
await getInflight(getApiInstance(), '/types/', {params: {tid: 1}});
await getInflight(getApiInstance(), '/types/', {params: {tid: 1}});
expect(networkMock.history.get.length).toBe(2);
});
it('rejects every caller and retries after a failure', async ()=>{
const attempts = [
getInflight(getApiInstance(), '/broken/'),
getInflight(getApiInstance(), '/broken/'),
];
await expect(attempts[0]).rejects.toThrow();
await expect(attempts[1]).rejects.toThrow();
expect(networkMock.history.get.length).toBe(1);
// The failed entry must not stick around and poison later callers.
await expect(
getInflight(getApiInstance(), '/broken/')
).rejects.toThrow();
expect(networkMock.history.get.length).toBe(2);
});
});