New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@lion/ajax

Package Overview
Dependencies
Maintainers
1
Versions
82
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lion/ajax - npm Package Compare versions

Comparing version 0.14.0 to 0.15.0

2

package.json
{
"name": "@lion/ajax",
"version": "0.14.0",
"version": "0.15.0",
"description": "Thin wrapper around fetch with support for interceptors.",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -17,3 +17,3 @@ /**

/**
* @type {Partial<AjaxConfig>}
* @type {AjaxConfig}
* @private

@@ -28,6 +28,6 @@ */

* Configures the Ajax instance
* @param {Partial<AjaxConfig>} config configuration for the Ajax instance
* @param {AjaxConfig} config configuration for the Ajax instance
*/
set options(arg: Partial<import("../types/types.js").AjaxConfig>);
get options(): Partial<import("../types/types.js").AjaxConfig>;
set options(arg: import("../types/types.js").AjaxConfig);
get options(): import("../types/types.js").AjaxConfig;
/** @param {RequestInterceptor} requestInterceptor */

@@ -34,0 +34,0 @@ addRequestInterceptor(requestInterceptor: RequestInterceptor): void;

@@ -26,3 +26,3 @@ /* eslint-disable consistent-return */

/**
* @type {Partial<AjaxConfig>}
* @type {AjaxConfig}
* @private

@@ -32,2 +32,3 @@ */

addAcceptLanguage: true,
addCaching: false,
xsrfCookieName: 'XSRF-TOKEN',

@@ -58,3 +59,3 @@ xsrfHeaderName: 'X-XSRF-TOKEN',

const { cacheOptions } = this.__config;
if (cacheOptions?.useCache) {
if (cacheOptions.useCache || this.__config.addCaching) {
const { cacheRequestInterceptor, cacheResponseInterceptor } = createCacheInterceptors(

@@ -71,3 +72,3 @@ cacheOptions.getCacheIdentifier,

* Configures the Ajax instance
* @param {Partial<AjaxConfig>} config configuration for the Ajax instance
* @param {AjaxConfig} config configuration for the Ajax instance
*/

@@ -74,0 +75,0 @@ set options(config) {

@@ -13,6 +13,6 @@ /**

export function resetCacheSession(cacheId: string): void;
export function extendCacheOptions({ useCache, methods, maxAge, requestIdFunction, invalidateUrls, invalidateUrlsRegex, }: CacheOptions): ValidatedCacheOptions;
export function validateCacheOptions({ useCache, methods, maxAge, requestIdFunction, invalidateUrls, invalidateUrlsRegex, }?: CacheOptions): void;
export function extendCacheOptions({ useCache, methods, maxAge, requestIdFunction, invalidateUrls, invalidateUrlsRegex, contentTypes, maxResponseSize, }: CacheOptions): ValidatedCacheOptions;
export function validateCacheOptions({ useCache, methods, maxAge, requestIdFunction, invalidateUrls, invalidateUrlsRegex, contentTypes, maxResponseSize, }?: CacheOptions): void;
export function invalidateMatchingCache(requestId: string, { invalidateUrls, invalidateUrlsRegex }: CacheOptions): void;
import Cache from "./Cache.js";
import PendingRequestStore from "./PendingRequestStore.js";

@@ -85,2 +85,4 @@ import './typedef.js';

invalidateUrlsRegex,
contentTypes,
maxResponseSize,
}) => ({

@@ -93,2 +95,4 @@ useCache,

invalidateUrlsRegex,
contentTypes,
maxResponseSize,
});

@@ -106,2 +110,4 @@

invalidateUrlsRegex,
contentTypes,
maxResponseSize,
} = {}) => {

@@ -118,6 +124,6 @@ if (useCache !== undefined && typeof useCache !== 'boolean') {

if (invalidateUrls !== undefined && !Array.isArray(invalidateUrls)) {
throw new Error('Property `invalidateUrls` must be an `Array` or `falsy`');
throw new Error('Property `invalidateUrls` must be an `Array` or `undefined`');
}
if (invalidateUrlsRegex !== undefined && !(invalidateUrlsRegex instanceof RegExp)) {
throw new Error('Property `invalidateUrlsRegex` must be a `RegExp` or `falsy`');
throw new Error('Property `invalidateUrlsRegex` must be a `RegExp` or `undefined`');
}

@@ -127,2 +133,8 @@ if (requestIdFunction !== undefined && typeof requestIdFunction !== 'function') {

}
if (contentTypes !== undefined && !Array.isArray(contentTypes)) {
throw new Error('Property `contentTypes` must be an `Array` or `undefined`');
}
if (maxResponseSize !== undefined && !Number.isFinite(maxResponseSize)) {
throw new Error('Property `maxResponseSize` must be a finite `number`');
}
};

@@ -129,0 +141,0 @@

@@ -14,2 +14,38 @@ /* eslint-disable no-param-reassign */

/**
* Tests whether the request method is supported according to the `cacheOptions`
* @param {ValidatedCacheOptions} cacheOptions
* @param {string} method
* @returns {boolean}
*/
const isMethodSupported = (cacheOptions, method) =>
cacheOptions.methods.includes(method.toLowerCase());
/**
* Tests whether the response content type is supported by the `contentTypes` whitelist
* @param {Response} response
* @param {CacheOptions} cacheOptions
* @returns {boolean} `true` if the contentTypes property is not an array, or if the value of the Content-Type header is in the array
*/
const isResponseContentTypeSupported = (response, { contentTypes } = {}) => {
if (!Array.isArray(contentTypes)) return true;
return contentTypes.includes(String(response.headers.get('Content-Type')));
};
/**
* Tests whether the response size is not too large to be cached according to the `maxResponseSize` property
* @param {Response} response
* @param {CacheOptions} cacheOptions
* @returns {boolean} `true` if the `maxResponseSize` property is not larger than zero, or if the Content-Length header is not present, or if the value of the header is not larger than the `maxResponseSize` property
*/
const isResponseSizeSupported = (response, { maxResponseSize } = {}) => {
const responseSize = +(response.headers.get('Content-Length') || 0);
if (!maxResponseSize) return true;
if (!responseSize) return true;
return responseSize <= maxResponseSize;
};
/**
* Request interceptor to return relevant cached requests

@@ -40,5 +76,4 @@ * @param {function(): string} getCacheId used to invalidate cache if identifier is changed

const requestId = cacheOptions.requestIdFunction(request);
const isMethodSupported = cacheOptions.methods.includes(request.method.toLowerCase());
if (!isMethodSupported) {
if (!isMethodSupported(cacheOptions, request.method)) {
invalidateMatchingCache(requestId, cacheOptions);

@@ -55,3 +90,7 @@ return request;

const cachedResponse = ajaxCache.get(requestId, cacheOptions.maxAge);
if (cachedResponse) {
if (
cachedResponse &&
isResponseContentTypeSupported(cachedResponse, cacheOptions) &&
isResponseSizeSupported(cachedResponse, cacheOptions)
) {
// Return the response from cache

@@ -87,9 +126,10 @@ request.cacheOptions = request.cacheOptions ?? { useCache: false };

const requestId = cacheOptions.requestIdFunction(response.request);
const isAlreadyFromCache = !!response.fromCache;
const isCacheActive = cacheOptions.useCache;
const isMethodSupported = cacheOptions.methods.includes(response.request?.method.toLowerCase());
if (!response.fromCache && isMethodSupported(cacheOptions, response.request.method)) {
const requestId = cacheOptions.requestIdFunction(response.request);
if (!isAlreadyFromCache && isCacheActive && isMethodSupported) {
if (isCurrentSessionId(response.request.cacheSessionId)) {
if (
isCurrentSessionId(response.request.cacheSessionId) &&
isResponseContentTypeSupported(response, cacheOptions) &&
isResponseSizeSupported(response, cacheOptions)
) {
// Cache the response

@@ -102,2 +142,3 @@ ajaxCache.set(requestId, response.clone());

}
return response;

@@ -104,0 +145,0 @@ };

@@ -11,5 +11,16 @@ import { expect } from '@open-wc/testing';

let responseId = 1;
const responseInit = () => ({
headers: {
// eslint-disable-next-line no-plusplus
'x-request-id': `${responseId++}`,
'content-type': 'application/json',
'x-custom-header': 'y-custom-value',
},
});
beforeEach(() => {
fetchStub = stub(window, 'fetch');
fetchStub.returns(Promise.resolve(new Response('mock response')));
fetchStub.callsFake(() => Promise.resolve(new Response('mock response', responseInit())));
ajax = new Ajax();

@@ -36,2 +47,3 @@ });

addAcceptLanguage: true,
addCaching: false,
xsrfCookieName: 'XSRF-TOKEN',

@@ -73,3 +85,4 @@ xsrfHeaderName: 'X-XSRF-TOKEN',

it('calls fetch with the given args, returning the result', async () => {
const response = await (await ajax.fetch('/foo', { method: 'POST' })).text();
const response = await ajax.fetch('/foo', { method: 'POST' });
const responseText = await response.text();

@@ -80,3 +93,5 @@ expect(fetchStub).to.have.been.calledOnce;

expect(request.method).to.equal('POST');
expect(response).to.equal('mock response');
expect(responseText).to.equal('mock response');
expect(response.headers.get('Content-Type')).to.equal('application/json');
expect(response.headers.get('X-Custom-Header')).to.equal('y-custom-value');
});

@@ -119,3 +134,3 @@

describe('fetchtJson', () => {
describe('fetchJson', () => {
beforeEach(() => {

@@ -132,5 +147,7 @@ fetchStub.returns(Promise.resolve(new Response('{}')));

it('decodes response from json', async () => {
fetchStub.returns(Promise.resolve(new Response('{"a":1,"b":2}')));
fetchStub.returns(Promise.resolve(new Response('{"a":1,"b":2}', responseInit())));
const response = await ajax.fetchJson('/foo');
expect(response.body).to.eql({ a: 1, b: 2 });
expect(response.response.headers.get('Content-Type')).to.equal('application/json');
expect(response.response.headers.get('X-Custom-Header')).to.equal('y-custom-value');
});

@@ -296,6 +313,17 @@

it('allows configuring cache interceptors on the Ajax config', async () => {
newCacheId();
it('does not add cache interceptors when useCache is turned off', () => {
const customAjax = new Ajax({
cacheOptions: {
maxAge: 100,
getCacheIdentifier,
},
});
expect(customAjax._requestInterceptors.length).to.equal(2);
expect(customAjax._responseInterceptors.length).to.equal(0);
});
it('adds cache interceptors when useCache is turned on', () => {
const customAjax = new Ajax({
cacheOptions: {
useCache: true,

@@ -307,25 +335,100 @@ maxAge: 100,

const clock = useFakeTimers({
shouldAdvanceTime: true,
expect(customAjax._requestInterceptors.length).to.equal(3);
expect(customAjax._responseInterceptors.length).to.equal(1);
});
it('adds cache interceptors when addCaching is turned on', () => {
const customAjax = new Ajax({
addCaching: true,
cacheOptions: {
maxAge: 100,
getCacheIdentifier,
},
});
// Smoke test 1: verify caching works
await customAjax.fetch('/foo');
expect(fetchStub.callCount).to.equal(1);
await customAjax.fetch('/foo');
expect(fetchStub.callCount).to.equal(1);
expect(customAjax._requestInterceptors.length).to.equal(3);
expect(customAjax._responseInterceptors.length).to.equal(1);
});
// Smoke test 2: verify caching is invalidated on non-get method
await customAjax.fetch('/foo', { method: 'POST' });
expect(fetchStub.callCount).to.equal(2);
await customAjax.fetch('/foo');
expect(fetchStub.callCount).to.equal(3);
describe('caching interceptors', async () => {
/**
* @type {Ajax}
*/
let customAjax;
// Smoke test 3: verify caching is invalidated after TTL has passed
await customAjax.fetch('/foo');
expect(fetchStub.callCount).to.equal(3);
clock.tick(101);
await customAjax.fetch('/foo');
expect(fetchStub.callCount).to.equal(4);
clock.restore();
beforeEach(async () => {
newCacheId();
customAjax = new Ajax({
cacheOptions: {
useCache: true,
maxAge: 100,
getCacheIdentifier,
},
});
});
it('works', async () => {
await customAjax.fetch('/foo');
const secondResponse = await customAjax.fetch('/foo');
expect(fetchStub.callCount).to.equal(1);
expect(await secondResponse.text()).to.equal('mock response');
expect(secondResponse.headers.get('X-Custom-Header')).to.equal('y-custom-value');
expect(secondResponse.headers.get('Content-Type')).to.equal('application/json');
});
it('works with fetchJson', async () => {
fetchStub.returns(Promise.resolve(new Response('{"a":1,"b":2}', responseInit())));
const firstResponse = await customAjax.fetchJson('/foo');
expect(firstResponse.body).to.deep.equal({ a: 1, b: 2 });
expect(firstResponse.response.headers.get('X-Custom-Header')).to.equal('y-custom-value');
expect(firstResponse.response.headers.get('Content-Type')).to.equal('application/json');
const secondResponse = await customAjax.fetchJson('/foo');
expect(fetchStub.callCount).to.equal(1);
expect(secondResponse.body).to.deep.equal({ a: 1, b: 2 });
expect(secondResponse.response.headers.get('X-Custom-Header')).to.equal('y-custom-value');
expect(secondResponse.response.headers.get('Content-Type')).to.equal('application/json');
});
it('is invalidated on non-get method', async () => {
await customAjax.fetch('/foo');
const secondResponse = await customAjax.fetch('/foo', { method: 'POST' });
expect(fetchStub.callCount).to.equal(2);
expect(await secondResponse.text()).to.equal('mock response');
expect(secondResponse.headers.get('X-Custom-Header')).to.equal('y-custom-value');
expect(secondResponse.headers.get('Content-Type')).to.equal('application/json');
const thirdResponse = await customAjax.fetch('/foo');
expect(fetchStub.callCount).to.equal(3);
expect(await thirdResponse.text()).to.equal('mock response');
expect(thirdResponse.headers.get('X-Custom-Header')).to.equal('y-custom-value');
expect(thirdResponse.headers.get('Content-Type')).to.equal('application/json');
});
it('is invalidated after TTL has passed', async () => {
const clock = useFakeTimers({
shouldAdvanceTime: true,
});
await customAjax.fetch('/foo');
const secondResponse = await customAjax.fetch('/foo');
expect(fetchStub.callCount).to.equal(1);
expect(await secondResponse.text()).to.equal('mock response');
expect(secondResponse.headers.get('X-Custom-Header')).to.equal('y-custom-value');
expect(secondResponse.headers.get('Content-Type')).to.equal('application/json');
clock.tick(101);
const thirdResponse = await customAjax.fetch('/foo');
expect(fetchStub.callCount).to.equal(2);
expect(await thirdResponse.text()).to.equal('mock response');
expect(thirdResponse.headers.get('X-Custom-Header')).to.equal('y-custom-value');
expect(thirdResponse.headers.get('Content-Type')).to.equal('application/json');
clock.restore();
});
});

@@ -332,0 +435,0 @@ });

@@ -77,2 +77,4 @@ // @ts-nocheck

invalidateUrlsRegex: invalidateUrlsRegexResult,
contentTypes,
maxResponseSize,
} = extendCacheOptions({ invalidateUrls, invalidateUrlsRegex });

@@ -86,2 +88,4 @@ // Assert

expect(invalidateUrlsRegexResult).to.equal(invalidateUrlsRegex);
expect(contentTypes).to.be.undefined;
expect(maxResponseSize).to.be.undefined;
});

@@ -134,2 +138,3 @@

});
it('accepts an empty object', () => {

@@ -140,2 +145,3 @@ expect(() => validateCacheOptions({})).not.to.throw(

});
describe('the useCache property', () => {

@@ -145,5 +151,7 @@ it('accepts a boolean', () => {

});
it('accepts undefined', () => {
expect(() => validateCacheOptions({ useCache: undefined })).not.to.throw;
});
it('does not accept anything else', () => {

@@ -156,2 +164,3 @@ // @ts-ignore

});
describe('the methods property', () => {

@@ -161,5 +170,7 @@ it('accepts an array with the value `get`', () => {

});
it('accepts undefined', () => {
expect(() => validateCacheOptions({ methods: undefined })).not.to.throw;
});
it('does not accept anything else', () => {

@@ -177,2 +188,3 @@ expect(() => validateCacheOptions({ methods: [] })).to.throw(

});
describe('the maxAge property', () => {

@@ -182,5 +194,7 @@ it('accepts a finite number', () => {

});
it('accepts undefined', () => {
expect(() => validateCacheOptions({ maxAge: undefined })).not.to.throw;
});
it('does not accept anything else', () => {

@@ -196,2 +210,3 @@ // @ts-ignore

});
describe('the invalidateUrls property', () => {

@@ -204,12 +219,15 @@ it('accepts an array', () => {

});
it('accepts undefined', () => {
expect(() => validateCacheOptions({ invalidateUrls: undefined })).not.to.throw;
});
it('does not accept anything else', () => {
// @ts-ignore
expect(() => validateCacheOptions({ invalidateUrls: 'not-an-array' })).to.throw(
'Property `invalidateUrls` must be an `Array` or `falsy`',
'Property `invalidateUrls` must be an `Array` or `undefined`',
);
});
});
describe('the invalidateUrlsRegex property', () => {

@@ -220,5 +238,7 @@ it('accepts a regular expression', () => {

});
it('accepts undefined', () => {
expect(() => validateCacheOptions({ invalidateUrlsRegex: undefined })).not.to.throw;
});
it('does not accept anything else', () => {

@@ -228,5 +248,6 @@ // @ts-ignore

validateCacheOptions({ invalidateUrlsRegex: 'a string is not a regex' }),
).to.throw('Property `invalidateUrlsRegex` must be a `RegExp` or `falsy`');
).to.throw('Property `invalidateUrlsRegex` must be a `RegExp` or `undefined`');
});
});
describe('the requestIdFunction property', () => {

@@ -239,5 +260,7 @@ it('accepts a function', () => {

});
it('accepts undefined', () => {
expect(() => validateCacheOptions({ requestIdFunction: undefined })).not.to.throw;
});
it('does not accept anything else', () => {

@@ -250,2 +273,41 @@ // @ts-ignore

});
describe('the contentTypes property', () => {
it('accepts an array', () => {
// @ts-ignore Typescript requires this to be an array of string, but this is not checked by validateCacheOptions
expect(() => validateCacheOptions({ contentTypes: [6, 'elements', 'in', 1, true, Array] }))
.not.to.throw;
});
it('accepts undefined', () => {
expect(() => validateCacheOptions({ contentTypes: undefined })).not.to.throw;
});
it('does not accept anything else', () => {
// @ts-ignore
expect(() => validateCacheOptions({ contentTypes: 'not-an-array' })).to.throw(
'Property `contentTypes` must be an `Array` or `undefined`',
);
});
});
describe('the maxResponseSize property', () => {
it('accepts a finite number', () => {
expect(() => validateCacheOptions({ maxResponseSize: 42 })).not.to.throw;
});
it('accepts undefined', () => {
expect(() => validateCacheOptions({ maxResponseSize: undefined })).not.to.throw;
});
it('does not accept anything else', () => {
// @ts-ignore
expect(() => validateCacheOptions({ maxResponseSize: 'string' })).to.throw(
'Property `maxResponseSize` must be a finite `number`',
);
expect(() => validateCacheOptions({ maxResponseSize: Infinity })).to.throw(
'Property `maxResponseSize` must be a finite `number`',
);
});
});
});

@@ -252,0 +314,0 @@

@@ -25,2 +25,4 @@ import { expect } from '@open-wc/testing';

let fetchStub;
/** @type {Response} */
let mockResponse;
const getCacheIdentifier = () => String(cacheId);

@@ -55,4 +57,5 @@ /** @type {sinon.SinonSpy} */

ajax = new Ajax();
mockResponse = new Response('mock response');
fetchStub = sinon.stub(window, 'fetch');
fetchStub.returns(Promise.resolve(new Response('mock response')));
fetchStub.resolves(mockResponse);
ajaxRequestSpy = sinon.spy(ajax, 'fetch');

@@ -155,4 +158,3 @@ });

// TODO: Check if this is the behaviour we want
it('all calls with non-default `maxAge` are cached proactively', async () => {
it('all calls are cached proactively', async () => {
// Given

@@ -163,3 +165,2 @@ newCacheId();

useCache: false,
maxAge: 100,
});

@@ -176,7 +177,3 @@

// When
await ajax.fetch('/test', {
cacheOptions: {
useCache: true,
},
});
await ajax.fetch('/test');

@@ -305,3 +302,3 @@ // Then

// @ts-ignore not an actual valid CacheResponse object
await cacheResponseInterceptor({ request: { method: 'get' } })
await cacheResponseInterceptor({ request: { method: 'get' }, headers: new Headers() })
.then(() => expect('everything').to.be.ok)

@@ -314,4 +311,257 @@ .catch(err =>

});
it('caches concurrent requests', async () => {
newCacheId();
const clock = sinon.useFakeTimers();
fetchStub.onFirstCall().returns(returnResponseOnTick(900, 1));
fetchStub.onSecondCall().returns(returnResponseOnTick(1900, 2));
addCacheInterceptors(ajax, {
useCache: true,
maxAge: 750,
});
const firstRequest = ajax.fetch('/test').then(r => r.text());
const concurrentFirstRequest1 = ajax.fetch('/test').then(r => r.text());
const concurrentFirstRequest2 = ajax.fetch('/test').then(r => r.text());
clock.tick(1000);
// firstRequest is cached at tick 1000 in the next line!
const firstResponses = await Promise.all([
firstRequest,
concurrentFirstRequest1,
concurrentFirstRequest2,
]);
expect(fetchStub.callCount).to.equal(1);
const cachedFirstRequest = ajax.fetch('/test').then(r => r.text());
clock.tick(500);
const cachedFirstResponse = await cachedFirstRequest;
expect(fetchStub.callCount).to.equal(1);
const secondRequest = ajax.fetch('/test').then(r => r.text());
const secondConcurrentRequest = ajax.fetch('/test').then(r => r.text());
clock.tick(1000);
const secondResponses = await Promise.all([secondRequest, secondConcurrentRequest]);
expect(fetchStub.callCount).to.equal(2);
expect(firstResponses).to.eql(['mock response 1', 'mock response 1', 'mock response 1']);
expect(cachedFirstResponse).to.equal('mock response 1');
expect(secondResponses).to.eql(['mock response 2', 'mock response 2']);
clock.restore();
});
it('preserves status and headers when returning cached response', async () => {
newCacheId();
fetchStub.returns(
Promise.resolve(
new Response('mock response', { status: 206, headers: { 'x-foo': 'x-bar' } }),
),
);
addCacheInterceptors(ajax, {
useCache: true,
maxAge: 100,
});
const response1 = await ajax.fetch('/test');
const response2 = await ajax.fetch('/test');
expect(fetchStub.callCount).to.equal(1);
expect(response1.status).to.equal(206);
expect(response1.headers.get('x-foo')).to.equal('x-bar');
expect(response2.status).to.equal(206);
expect(response2.headers.get('x-foo')).to.equal('x-bar');
});
it('does save to the cache when `contentTypes` is specified and a supported content type is returned', async () => {
// Given
newCacheId();
mockResponse.headers.set('content-type', 'application/xml');
addCacheInterceptors(ajax, {
useCache: true,
contentTypes: ['application/json', 'application/xml'],
});
// When
await ajax.fetch('/test');
await ajax.fetch('/test');
// Then
expect(fetchStub.callCount).to.equal(1);
});
it('does save to the cache when `maxResponseSize` is specified and the response size is within the threshold', async () => {
// Given
newCacheId();
mockResponse.headers.set('content-length', '20000');
addCacheInterceptors(ajax, {
useCache: true,
maxResponseSize: 50000,
});
// When
await ajax.fetch('/test');
await ajax.fetch('/test');
// Then
expect(fetchStub.callCount).to.equal(1);
});
it('does save to the cache when `maxResponseSize` is specified and the response size is unknown', async () => {
// Given
newCacheId();
addCacheInterceptors(ajax, {
useCache: true,
maxResponseSize: 50000,
});
// When
await ajax.fetch('/test');
await ajax.fetch('/test');
// Then
expect(fetchStub.callCount).to.equal(1);
});
});
describe('Bypassing the cache', () => {
it('caches response but does not return it when expiration time is 0', async () => {
newCacheId();
addCacheInterceptors(ajax, {
useCache: true,
maxAge: 0,
});
const clock = sinon.useFakeTimers();
await ajax.fetch('/test');
expect(ajaxRequestSpy.calledOnce).to.be.true;
expect(ajaxRequestSpy.calledWith('/test')).to.be.true;
clock.tick(1);
await ajax.fetch('/test');
clock.restore();
expect(fetchStub.callCount).to.equal(2);
});
it('does not use cache when cacheOption `useCache: false` is passed to fetch method', async () => {
// Given
newCacheId();
addCacheInterceptors(ajax, { useCache: true });
// When
await ajax.fetch('/test');
await ajax.fetch('/test');
// Then
expect(fetchStub.callCount).to.equal(1);
// When
await ajax.fetch('/test', { cacheOptions: { useCache: false } });
// Then
expect(fetchStub.callCount).to.equal(2);
});
it('does not save to the cache when `contentTypes` is specified and an unsupported content type is returned', async () => {
// Given
newCacheId();
mockResponse.headers.set('content-type', 'text/html');
addCacheInterceptors(ajax, {
useCache: true,
contentTypes: ['application/json', 'application/xml'],
});
// When
await ajax.fetch('/test');
await ajax.fetch('/test', { cacheOptions: { contentTypes: ['text/html'] } });
// Then
expect(fetchStub.callCount).to.equal(2);
});
it('does not read from the cache when `contentTypes` is specified and an unsupported content type is returned', async () => {
// Given
newCacheId();
mockResponse.headers.set('content-type', 'application/json');
addCacheInterceptors(ajax, {
useCache: true,
contentTypes: ['application/json', 'application/xml'],
});
// When
await ajax.fetch('/test');
await ajax.fetch('/test');
// Then
expect(fetchStub.callCount).to.equal(1);
// When
await ajax.fetch('/test', { cacheOptions: { contentTypes: [] } });
// Then
expect(fetchStub.callCount).to.equal(2);
});
it('does not save to the cache when `maxResponseSize` is specified and a larger content-length is specified in the response', async () => {
// Given
newCacheId();
mockResponse.headers.set('content-length', '80000');
addCacheInterceptors(ajax, {
useCache: true,
maxResponseSize: 50000,
});
// When
await ajax.fetch('/test');
await ajax.fetch('/test', { cacheOptions: { maxResponseSize: 100000 } });
// Then
expect(fetchStub.callCount).to.equal(2);
});
it('does not read from the cache when `maxResponseSize` is specified and a larger content-length is specified in the response', async () => {
// Given
newCacheId();
mockResponse.headers.set('content-length', '80000');
addCacheInterceptors(ajax, {
useCache: true,
maxResponseSize: 100000,
});
// When
await ajax.fetch('/test');
await ajax.fetch('/test');
// Then
expect(fetchStub.callCount).to.equal(1);
// When
await ajax.fetch('/test', { cacheOptions: { maxResponseSize: 50000 } });
// Then
expect(fetchStub.callCount).to.equal(2);
});
});
describe('Cache invalidation', () => {

@@ -459,97 +709,2 @@ it('previously cached data has to be invalidated when regex invalidation rule triggered', async () => {

it('caches response but does not return it when expiration time is 0', async () => {
newCacheId();
addCacheInterceptors(ajax, {
useCache: true,
maxAge: 0,
});
const clock = sinon.useFakeTimers();
await ajax.fetch('/test');
expect(ajaxRequestSpy.calledOnce).to.be.true;
expect(ajaxRequestSpy.calledWith('/test')).to.be.true;
clock.tick(1);
await ajax.fetch('/test');
clock.restore();
expect(fetchStub.callCount).to.equal(2);
});
it('does not use cache when cacheOption `useCache: false` is passed to fetch method', async () => {
// Given
addCacheInterceptors(ajax, { useCache: true });
// When
await ajax.fetch('/test');
await ajax.fetch('/test');
// Then
expect(fetchStub.callCount).to.equal(1);
// When
await ajax.fetch('/test', { cacheOptions: { useCache: false } });
// Then
expect(fetchStub.callCount).to.equal(2);
});
it('caches concurrent requests', async () => {
newCacheId();
const clock = sinon.useFakeTimers();
fetchStub.onFirstCall().returns(returnResponseOnTick(900, 1));
fetchStub.onSecondCall().returns(returnResponseOnTick(1900, 2));
addCacheInterceptors(ajax, {
useCache: true,
maxAge: 750,
});
const firstRequest = ajax.fetch('/test').then(r => r.text());
const concurrentFirstRequest1 = ajax.fetch('/test').then(r => r.text());
const concurrentFirstRequest2 = ajax.fetch('/test').then(r => r.text());
clock.tick(1000);
// firstRequest is cached at tick 1000 in the next line!
const firstResponses = await Promise.all([
firstRequest,
concurrentFirstRequest1,
concurrentFirstRequest2,
]);
expect(fetchStub.callCount).to.equal(1);
const cachedFirstRequest = ajax.fetch('/test').then(r => r.text());
clock.tick(500);
const cachedFirstResponse = await cachedFirstRequest;
expect(fetchStub.callCount).to.equal(1);
const secondRequest = ajax.fetch('/test').then(r => r.text());
const secondConcurrentRequest = ajax.fetch('/test').then(r => r.text());
clock.tick(1000);
const secondResponses = await Promise.all([secondRequest, secondConcurrentRequest]);
expect(fetchStub.callCount).to.equal(2);
expect(firstResponses).to.eql(['mock response 1', 'mock response 1', 'mock response 1']);
expect(cachedFirstResponse).to.equal('mock response 1');
expect(secondResponses).to.eql(['mock response 2', 'mock response 2']);
});
it('discards responses that are requested in a different cache session', async () => {

@@ -580,25 +735,3 @@ newCacheId();

});
it('preserves status and headers when returning cached response', async () => {
newCacheId();
fetchStub.returns(
Promise.resolve(
new Response('mock response', { status: 206, headers: { 'x-foo': 'x-bar' } }),
),
);
addCacheInterceptors(ajax, {
useCache: true,
maxAge: 100,
});
const response1 = await ajax.fetch('/test');
const response2 = await ajax.fetch('/test');
expect(fetchStub.callCount).to.equal(1);
expect(response1.status).to.equal(206);
expect(response1.headers.get('x-foo')).to.equal('x-bar');
expect(response2.status).to.equal(206);
expect(response2.headers.get('x-foo')).to.equal('x-bar');
});
});
});

@@ -15,2 +15,3 @@ /**

addAcceptLanguage: boolean;
addCaching: boolean;
xsrfCookieName: string | null;

@@ -43,2 +44,4 @@ xsrfHeaderName: string | null;

requestIdFunction?: RequestIdFunction;
contentTypes?: string[];
maxResponseSize?: number;
}

@@ -45,0 +48,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc