diff --git a/modules/mobianRtdProvider.js b/modules/mobianRtdProvider.js index d0eb5880ba..20a3f5997f 100644 --- a/modules/mobianRtdProvider.js +++ b/modules/mobianRtdProvider.js @@ -59,20 +59,28 @@ export const CONTEXT_KEYS = [ const AP_KEYS = ['a0', 'a1', 'p0', 'p1']; +export const MAX_CACHE_SIZE = 10; + // eslint-disable-next-line no-restricted-syntax const logMessage = (...args) => { _logMessage('Mobian', ...args); }; -function makeMemoizedFetch() { - let cachedResponse = null; +export function makeMemoizedFetch(maxSize = MAX_CACHE_SIZE) { + const sanitizedMaxSize = (Number.isFinite(maxSize) && maxSize >= 1) ? Math.floor(maxSize) : MAX_CACHE_SIZE; + const cache = new Map(); return async function () { - if (cachedResponse) { - return Promise.resolve(cachedResponse); + const pageUrl = window.location.href; + if (cache.has(pageUrl)) { + return Promise.resolve(cache.get(pageUrl)); } try { const response = await fetchContextData(); - cachedResponse = makeDataFromResponse(response); + const cachedResponse = makeDataFromResponse(response); + if (cache.size >= sanitizedMaxSize) { + cache.delete(cache.keys().next().value); + } + cache.set(pageUrl, cachedResponse); return cachedResponse; } catch (error) { logMessage('error', error); diff --git a/test/spec/modules/mobianRtdProvider_spec.js b/test/spec/modules/mobianRtdProvider_spec.js index 0794e99151..c0bc5f5a83 100644 --- a/test/spec/modules/mobianRtdProvider_spec.js +++ b/test/spec/modules/mobianRtdProvider_spec.js @@ -8,6 +8,7 @@ import { CATEGORIES, EMOTIONS, GENRES, + MAX_CACHE_SIZE, RISK, SENTIMENT, THEMES, @@ -16,6 +17,7 @@ import { fetchContextData, getConfig, getContextData, + makeMemoizedFetch, makeContextDataToKeyValuesReducer, makeDataFromResponse, setTargeting, @@ -278,4 +280,93 @@ describe('Mobian RTD Submodule', function () { expect(keyValuesObject).to.deep.equal(mockKeyValues); }); }); + + describe('makeMemoizedFetch cache eviction', function () { + it('should evict the oldest entry when cache exceeds maxSize', async function () { + const maxSize = 2; + let fetchCount = 0; + ajaxStub = sinon.stub(ajax, 'ajaxBuilder').returns(function (url, callbacks) { + fetchCount++; + callbacks.success(mockResponse); + }); + + const memoizedFetch = makeMemoizedFetch(maxSize); + + await memoizedFetch(); + expect(fetchCount).to.equal(1); + + await memoizedFetch(); + expect(fetchCount).to.equal(1); + + const originalHref = window.location.href; + try { + history.pushState({}, '', '/page2'); + await memoizedFetch(); + expect(fetchCount).to.equal(2); + + history.pushState({}, '', '/page3'); + await memoizedFetch(); + expect(fetchCount).to.equal(3); + + history.pushState({}, '', originalHref); + await memoizedFetch(); + expect(fetchCount).to.equal(4); + } finally { + history.replaceState({}, '', originalHref); + } + }); + + it('should fall back to MAX_CACHE_SIZE for invalid maxSize values', async function () { + ajaxStub = sinon.stub(ajax, 'ajaxBuilder').returns(function (url, callbacks) { + callbacks.success(mockResponse); + }); + + const invalidValues = [NaN, Infinity, -Infinity, 0, -1, 'abc', null, undefined, {}, []]; + const originalHref = window.location.href; + try { + for (const invalid of invalidValues) { + const memoizedFetch = makeMemoizedFetch(invalid); + + for (let i = 0; i < MAX_CACHE_SIZE; i++) { + history.pushState({}, '', `/fallback-${invalid}-${i}`); + await memoizedFetch(); + } + + const callsBefore = ajax.ajaxBuilder.callCount; + history.pushState({}, '', `/fallback-${invalid}-0`); + await memoizedFetch(); + expect(ajax.ajaxBuilder.callCount).to.equal(callsBefore, + `Expected cached hit for maxSize=${invalid}, but a new fetch was made`); + } + } finally { + history.pushState({}, '', originalHref); + } + }); + + it('should floor fractional maxSize to an integer', async function () { + let fetchCount = 0; + ajaxStub = sinon.stub(ajax, 'ajaxBuilder').returns(function (url, callbacks) { + fetchCount++; + callbacks.success(mockResponse); + }); + + const memoizedFetch = makeMemoizedFetch(1.9); + const originalHref = window.location.href; + + try { + await memoizedFetch(); + expect(fetchCount).to.equal(1); + + history.pushState({}, '', '/fractional-page2'); + await memoizedFetch(); + expect(fetchCount).to.equal(2); + + history.pushState({}, '', originalHref); + await memoizedFetch(); + expect(fetchCount).to.equal(3); + } finally { + history.pushState({}, '', originalHref); + } + }); + }); });