If session cache Id changes, reset the cache and pending requests

This commit is contained in:
Alex Thirlwall 2024-04-18 15:17:36 +02:00 committed by Thijs Louisse
parent bc587544ec
commit df8bf58f03
6 changed files with 84 additions and 3 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ajax': patch
---
Reset cache and pending requests when cache session ID changes

View file

@ -55,6 +55,7 @@ export default class PendingRequestStore {
}
reset() {
this.resolveMatching(/.*/);
this._pendingRequests = {};
}
}

View file

@ -32,6 +32,15 @@ export const pendingRequestStore = new PendingRequestStore();
*/
export const isCurrentSessionId = cacheId => cacheId === cacheSessionId;
/**
* Sets the current cache session ID.
*
* @param {string} id The id that will be tied to the current session
*/
export const setCacheSessionId = id => {
cacheSessionId = id;
};
/**
* Resets the cache session when the cacheId changes.
*
@ -43,7 +52,7 @@ export const resetCacheSession = cacheId => {
throw new Error('Invalid cache identifier');
}
if (!isCurrentSessionId(cacheId)) {
cacheSessionId = cacheId;
setCacheSessionId(cacheId);
ajaxCache.reset();
pendingRequestStore.reset();
}

View file

@ -96,6 +96,12 @@ const createCacheRequestInterceptor =
if (pendingRequest) {
// there is another concurrent request, wait for it to finish
await pendingRequest;
// If session ID changes while waiting for the pending request to complete,
// then do not read the cache.
if (!isCurrentSessionId(request.cacheSessionId)) {
return request;
}
}
const cachedResponse = ajaxCache.get(requestId, { maxAge, maxResponseSize });

View file

@ -503,7 +503,7 @@ describe('Ajax', () => {
expect(customAjax._responseInterceptors.length).to.equal(1);
});
describe('caching interceptors', async () => {
describe('Caching interceptors: synchronous getCacheIdentifier tests', async () => {
/**
* @type {Ajax}
*/
@ -530,6 +530,30 @@ describe('Ajax', () => {
expect(secondResponse.headers.get('Content-Type')).to.equal('application/json');
});
it('resets the cache if the cache ID changes', async () => {
/* Three calls to the same endpoint should result in a
single fetchStubCall due to caching */
await customAjax.fetch('/foo');
await customAjax.fetch('/foo');
await customAjax.fetch('/foo');
expect(fetchStub.callCount).to.equal(1);
newCacheId();
await customAjax.fetch('/foo');
/* The newCacheId call should reset the cache, thereby adding an
extra call to the fetchStub call count. */
expect(fetchStub.callCount).to.equal(2);
});
it('Completes pending requests when the cache ID changes', async () => {
const requestOne = customAjax.fetch('/foo').then(() => 'completedRequestOne');
newCacheId();
const requestTwo = customAjax.fetch('/foo').then(() => 'completedRequestTwo');
expect(await requestOne).to.equal('completedRequestOne');
expect(await requestTwo).to.equal('completedRequestTwo');
});
it('works with fetchJson', async () => {
fetchStub.returns(Promise.resolve(new Response('{"a":1,"b":2}', responseInit())));

View file

@ -4,7 +4,12 @@ import { Ajax, createCacheInterceptors } from '@lion/ajax';
import { isResponseContentTypeSupported } from '../../src/interceptors/cacheInterceptors.js';
// TODO: these are private API? should they be exposed? if not why do we test them?
import { extendCacheOptions, resetCacheSession, ajaxCache } from '../../src/cacheManager.js';
import {
extendCacheOptions,
resetCacheSession,
ajaxCache,
setCacheSessionId,
} from '../../src/cacheManager.js';
const MOCK_RESPONSE = 'mock response';
@ -437,6 +442,37 @@ describe('cache interceptors', () => {
expect(fetchStub.callCount).to.equal(1);
});
it('Does not use cached request when session ID changes during processing a pending request', async () => {
addCacheInterceptors(ajax, {
useCache: true,
maxAge: 750,
});
// Reset cache
newCacheId();
/* Bump sessionID manually in an injected response interceptor.
This will simulate the cache session ID getting changed while
waiting for a pending request */
ajax._responseInterceptors.unshift(async (/** @type {Response} */ response) => {
newCacheId();
setCacheSessionId(getCacheIdentifier());
return response;
});
const requestOne = ajax.fetch('/foo').then(() => 'completedRequestOne');
const requestTwo = ajax.fetch('/foo').then(() => 'completedRequestTwo');
expect(await requestOne).to.equal('completedRequestOne');
// At this point the response interceptor of requestOne has called setCacheSessionId
expect(await requestTwo).to.equal('completedRequestTwo');
/* Neither call should use the cache. During the first call there is no cache entry for '/foo'.
During the second call there is, but since the first call's injected interceptor has bumped
the cache session ID, it shouldn't use the cached response. */
expect(fetchStub.callCount).to.equal(2);
});
it('does save to the cache when `maxResponseSize` is specified and the response size is within the threshold', async () => {
// Given
newCacheId();