Socket
Socket
Sign inDemoInstall

axios-cache-interceptor

Package Overview
Dependencies
Maintainers
1
Versions
78
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

axios-cache-interceptor - npm Package Compare versions

Comparing version 0.5.1 to 0.6.0

dist/storage/browser.js

8

dist/cache/create.js

@@ -22,3 +22,3 @@ "use strict";

const axiosCache = axios;
axiosCache.storage = storage || new memory_1.MemoryStorage();
axiosCache.storage = storage || new memory_1.MemoryAxiosStorage({});
axiosCache.generateKey = generateKey || key_generator_1.defaultKeyGenerator;

@@ -38,5 +38,5 @@ axiosCache.waiting = waiting || {};

methods: ['get'],
cachePredicate: {
statusCheck: [200, 399]
},
cachePredicate: { statusCheck: [200, 399] },
etag: false,
modifiedSince: false,
update: {},

@@ -43,0 +43,0 @@ ...cacheOptions

@@ -5,30 +5,43 @@ "use strict";

const cache_control_1 = require("@tusbar/cache-control");
const defaultHeaderInterpreter = (headers) => {
const cacheControl = headers?.['cache-control'];
if (!cacheControl) {
// Checks if Expires header is present
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires
const expires = headers?.['expires'];
if (expires) {
const milliseconds = Date.parse(expires) - Date.now();
if (milliseconds > 0) {
return milliseconds;
}
else {
return false;
}
}
return undefined;
const headers_1 = require("../util/headers");
const defaultHeaderInterpreter = (headers = {}) => {
if (headers_1.Header.CacheControl in headers) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return interpretCacheControl(headers[headers_1.Header.CacheControl], headers);
}
const { noCache, noStore, mustRevalidate, maxAge } = (0, cache_control_1.parse)(cacheControl);
if (headers_1.Header.Expires in headers) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return interpretExpires(headers[headers_1.Header.Expires], headers);
}
return undefined;
};
exports.defaultHeaderInterpreter = defaultHeaderInterpreter;
const interpretExpires = (expires) => {
const milliseconds = Date.parse(expires) - Date.now();
return milliseconds >= 0 ? milliseconds : false;
};
const interpretCacheControl = (cacheControl, headers) => {
const { noCache, noStore, mustRevalidate, maxAge, immutable } = (0, cache_control_1.parse)(cacheControl);
// Header told that this response should not be cached.
if (noCache || noStore || mustRevalidate) {
if (noCache || noStore) {
return false;
}
if (!maxAge) {
return undefined;
if (immutable) {
// 1 year is sufficient, as Infinity may cause more problems.
// It might not be the best way, but a year is better than none.
return 1000 * 60 * 60 * 24 * 365;
}
return maxAge * 1000;
// Already out of date, for cache can be saved, but must be requested again
if (mustRevalidate) {
return 0;
}
if (maxAge) {
const age = headers[headers_1.Header.Age];
if (!age) {
return maxAge * 1000;
}
return maxAge * 1000 - Number(age) * 1000;
}
return undefined;
};
exports.defaultHeaderInterpreter = defaultHeaderInterpreter;
//# sourceMappingURL=interpreter.js.map

@@ -18,4 +18,5 @@ "use strict";

__exportStar(require("./interceptors/types"), exports);
__exportStar(require("./storage/storage"), exports);
__exportStar(require("./storage/types"), exports);
__exportStar(require("./util/types"), exports);
//# sourceMappingURL=index.js.map

@@ -5,2 +5,3 @@ "use strict";

const deferred_1 = require("typed-core/dist/promises/deferred");
const headers_1 = require("../util/headers");
class CacheRequestInterceptor {

@@ -13,9 +14,10 @@ constructor(axios) {

this.onFulfilled = async (config) => {
// Skip cache
if (config.cache === false) {
return config;
}
// Only cache specified methods
const allowedMethods = config.cache?.methods || this.axios.defaults.cache.methods;
if (!allowedMethods.some((method) => (config.method || 'get').toLowerCase() == method)) {
// merge defaults with per request configuration
config.cache = { ...this.axios.defaults.cache, ...config.cache };
if (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
!this.isMethodAllowed(config.method, config.cache)) {
return config;

@@ -27,3 +29,3 @@ }

// Not cached, continue the request, and mark it as fetching
emptyState: if (cache.state == 'empty') {
emptyOrStale: if (cache.state == 'empty' || cache.state === 'stale') {
/**

@@ -36,3 +38,3 @@ * This checks for simultaneous access to a new key. The js

cache = (await this.axios.storage.get(key));
break emptyState;
break emptyOrStale;
}

@@ -48,4 +50,9 @@ // Create a deferred to resolve other requests for the same key when it's completed

state: 'loading',
ttl: config.cache?.ttl
data: cache.data
});
if (cache.state === 'stale') {
//@ts-expect-error type infer couldn't resolve this
this.setRevalidationHeaders(cache, config);
}
config.validateStatus = CacheRequestInterceptor.createValidateStatus(config.validateStatus);
return config;

@@ -56,6 +63,4 @@ }

const deferred = this.axios.waiting[key];
/**
* If the deferred is undefined, means that the outside has
* removed that key from the waiting list
*/
// Just in case, the deferred doesn't exists.
/* istanbul ignore if 'really hard to test' */
if (!deferred) {

@@ -92,5 +97,43 @@ await this.axios.storage.remove(key);

};
this.isMethodAllowed = (method, properties) => {
const requestMethod = method.toLowerCase();
for (const method of properties.methods || []) {
if (method.toLowerCase() === requestMethod) {
return true;
}
}
return false;
};
this.setRevalidationHeaders = (cache, config) => {
config.headers || (config.headers = {});
const { etag, modifiedSince } = config.cache;
if (etag) {
const etagValue = etag === true ? cache.data?.headers[headers_1.Header.ETag] : etag;
if (etagValue) {
config.headers[headers_1.Header.IfNoneMatch] = etagValue;
}
}
if (modifiedSince) {
config.headers[headers_1.Header.IfModifiedSince] =
modifiedSince === true
? // If last-modified is not present, use the createdAt timestamp
cache.data.headers[headers_1.Header.LastModified] ||
new Date(cache.createdAt).toUTCString()
: modifiedSince.toUTCString();
}
};
}
}
exports.CacheRequestInterceptor = CacheRequestInterceptor;
/**
* Creates a new validateStatus function that will use the one
* already used and also accept status code 304.
*/
CacheRequestInterceptor.createValidateStatus = (oldValidate) => {
return (status) => {
return oldValidate
? oldValidate(status) || status === 304
: (status >= 200 && status < 300) || status === 304;
};
};
//# sourceMappingURL=request.js.map

@@ -6,2 +6,3 @@ "use strict";

const cache_predicate_1 = require("../util/cache-predicate");
const headers_1 = require("../util/headers");
const update_cache_1 = require("../util/update-cache");

@@ -14,34 +15,4 @@ class CacheResponseInterceptor {

};
this.testCachePredicate = (response, cache) => {
const cachePredicate = cache?.cachePredicate || this.axios.defaults.cache.cachePredicate;
return ((typeof cachePredicate === 'function' && cachePredicate(response)) ||
(typeof cachePredicate === 'object' &&
(0, cache_predicate_1.checkPredicateObject)(response, cachePredicate)));
};
/**
* Rejects cache for this response. Also update the waiting list for
* this key by rejecting it.
*/
this.rejectResponse = async (key) => {
// Update the cache to empty to prevent infinite loading state
await this.axios.storage.remove(key);
// Reject the deferred if present
this.axios.waiting[key]?.reject();
delete this.axios.waiting[key];
};
this.onFulfilled = async (axiosResponse) => {
const key = this.axios.generateKey(axiosResponse.config);
const response = {
id: key,
/**
* The request interceptor response.cache will return true or
* undefined. And true only when the response was cached.
*/
cached: axiosResponse.cached || false,
...axiosResponse
};
// Skip cache
if (response.config.cache === false) {
return { ...response, cached: false };
}
const response = this.cachedResponse(axiosResponse);
// Response is already cached

@@ -51,45 +22,109 @@ if (response.cached) {

}
const cache = await this.axios.storage.get(key);
/**
* From now on, the cache and response represents the state of the
* first response to a request, which has not yet been cached or
* processed before.
*/
if (cache.state !== 'loading') {
// Skip cache
// either false or weird behavior, config.cache should always exists, from global config merge at least
if (!response.config.cache) {
return { ...response, cached: false };
}
const cacheConfig = response.config.cache;
const cache = await this.axios.storage.get(response.id);
if (
// If the request interceptor had a problem
cache.state === 'stale' ||
cache.state === 'empty' ||
// Should not hit here because of later response.cached check
cache.state === 'cached') {
return response;
}
// Config told that this response should be cached.
if (!this.testCachePredicate(response, response.config.cache)) {
await this.rejectResponse(key);
if (
// For 'loading' values (post stale), this check was already run in the past.
!cache.data &&
!this.testCachePredicate(response, cacheConfig)) {
await this.rejectResponse(response.id);
return response;
}
let ttl = response.config.cache?.ttl || this.axios.defaults.cache.ttl;
if (response.config.cache?.interpretHeader) {
// avoid remnant headers from remote server to break implementation
delete response.headers[headers_1.Header.XAxiosCacheEtag];
delete response.headers[headers_1.Header.XAxiosCacheLastModified];
if (cacheConfig.etag && cacheConfig.etag !== true) {
response.headers[headers_1.Header.XAxiosCacheEtag] = cacheConfig.etag;
}
if (cacheConfig.modifiedSince) {
response.headers[headers_1.Header.XAxiosCacheLastModified] =
cacheConfig.modifiedSince === true
? 'use-cache-timestamp'
: cacheConfig.modifiedSince.toUTCString();
}
let ttl = cacheConfig.ttl || -1; // always set from global config
if (cacheConfig?.interpretHeader) {
const expirationTime = this.axios.headerInterpreter(response.headers);
// Cache should not be used
if (expirationTime === false) {
await this.rejectResponse(key);
await this.rejectResponse(response.id);
return response;
}
ttl = expirationTime ? expirationTime : ttl;
ttl = expirationTime || expirationTime === 0 ? expirationTime : ttl;
}
const data = response.status == 304 && cache.data
? (() => {
// Rust syntax <3
response.cached = true;
response.data = cache.data.data;
response.status = cache.data.status;
response.statusText = cache.data.statusText;
// We may have new headers.
response.headers = {
...cache.data.headers,
...response.headers
};
return cache.data;
})()
: (0, object_1.extract)(response, ['data', 'headers', 'status', 'statusText']);
const newCache = {
state: 'cached',
ttl: ttl,
ttl,
createdAt: Date.now(),
data: (0, object_1.extract)(response, ['data', 'headers', 'status', 'statusText'])
data
};
// Update other entries before updating himself
if (response.config.cache?.update) {
(0, update_cache_1.updateCache)(this.axios.storage, response.data, response.config.cache.update);
if (cacheConfig?.update) {
(0, update_cache_1.updateCache)(this.axios.storage, response.data, cacheConfig.update);
}
const deferred = this.axios.waiting[key];
const deferred = this.axios.waiting[response.id];
// Resolve all other requests waiting for this response
await deferred?.resolve(newCache.data);
delete this.axios.waiting[key];
delete this.axios.waiting[response.id];
// Define this key as cache on the storage
await this.axios.storage.set(key, newCache);
await this.axios.storage.set(response.id, newCache);
// Return the response with cached as false, because it was not cached at all
return response;
};
this.testCachePredicate = (response, cache) => {
const cachePredicate = cache.cachePredicate;
return ((typeof cachePredicate === 'function' && cachePredicate(response)) ||
(typeof cachePredicate === 'object' &&
(0, cache_predicate_1.checkPredicateObject)(response, cachePredicate)));
};
/**
* Rejects cache for this response. Also update the waiting list for
* this key by rejecting it.
*/
this.rejectResponse = async (key) => {
// Update the cache to empty to prevent infinite loading state
await this.axios.storage.remove(key);
// Reject the deferred if present
this.axios.waiting[key]?.reject();
delete this.axios.waiting[key];
};
this.cachedResponse = (response) => {
return {
id: this.axios.generateKey(response.config),
/**
* The request interceptor response.cache will return true or
* undefined. And true only when the response was cached.
*/
cached: response.cached || false,
...response
};
};
}

@@ -96,0 +131,0 @@ }

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MemoryStorage = void 0;
const util_1 = require("./util");
class MemoryStorage {
constructor() {
this.storage = new Map();
this.get = async (key) => {
const value = this.storage.get(key);
if (!value) {
return { state: 'empty' };
}
if ((0, util_1.isCacheValid)(value) === false) {
this.remove(key);
return { state: 'empty' };
}
return value;
exports.MemoryAxiosStorage = void 0;
const storage_1 = require("./storage");
class MemoryAxiosStorage extends storage_1.AxiosStorage {
constructor(storage = {}) {
super();
this.storage = storage;
this.find = async (key) => {
return this.storage[key] || { state: 'empty' };
};
this.set = async (key, value) => {
this.storage.set(key, value);
this.storage[key] = value;
};
this.remove = async (key) => {
this.storage.delete(key);
delete this.storage[key];
};
}
}
exports.MemoryStorage = MemoryStorage;
exports.MemoryAxiosStorage = MemoryAxiosStorage;
//# sourceMappingURL=memory.js.map
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateCache = void 0;
/**
* Function to update all caches, from CacheProperties.update, with
* the new data.
*/
async function updateCache(storage, data, entries) {

@@ -8,3 +12,3 @@ for (const cacheKey in entries) {

const value = entries[cacheKey];
if (value == 'delete') {
if (value === 'delete') {
await storage.remove(cacheKey);

@@ -11,0 +15,0 @@ continue;

{
"name": "axios-cache-interceptor",
"version": "0.5.1",
"version": "0.6.0",
"description": "Cache interceptor for axios",

@@ -5,0 +5,0 @@ "main": "./dist/index.js",

@@ -90,3 +90,4 @@ <br />

- [Getting Started](#getting-started)
- [What we support](#what-we-support)
- [Support list](#support-list-1)
- [Compiled code](#compiled-code)
- [Basic Knowledge](#basic-knowledge)

@@ -110,2 +111,4 @@ - [Request id](#request-id)

- [request.cache.update](#requestcacheupdate)
- [request.cache.etag](#requestcacheetag)
- [request.cache.modifiedSince](#requestcachemodifiedsince)
- [License](#license)

@@ -178,3 +181,3 @@ - [Contact](#contact)

## What we support
## Support list

@@ -185,6 +188,18 @@ - [x] Concurrent requests

- [x] Header interpretation
- [x] ETag and If-Modified-Since cache support
- [x] Infinity storage options
- [x] Cache revalidation from responses
- [ ] External storages, like redis
- [x] Support for external storages
## Compiled code
Currently, the typescript compiler is only used to remove types from code, emitting almost
the same output code as if this library were written in javascript. Nowadays, it is
practically mandatory to use some pre-processor, like Babel. So for the support of
multiple users in the browser, we recommend you to use it as well.
Current target: **ES2020**
Build options: **[`tsconfig.json`](/tsconfig.json)**
## Basic Knowledge

@@ -391,2 +406,16 @@

### request.cache.etag
If the request should handle `ETag` and `If-None-Match support`. Use a string to force a
custom static value or true to use the previous response ETag. To use `true` (automatic
etag handling), `interpretHeader` option must be set to `true`. Default: `false`
### request.cache.modifiedSince
Use `If-Modified-Since` header in this request. Use a date to force a custom static value
or true to use the last cached timestamp. If never cached before, the header is not set.
If `interpretHeader` is set and a `Last-Modified` header is sent then value from that
header is used, otherwise cache creation timestamp will be sent in `If-Modified-Since`.
Default: `true`
<br />

@@ -393,0 +422,0 @@

@@ -66,3 +66,3 @@ import type {

<T = any, D = any, R = CacheAxiosResponse<T, D>>(
config?: CacheRequestConfig<D>
config: CacheRequestConfig<D>
): Promise<R>;

@@ -69,0 +69,0 @@ /**

import type { Method } from 'axios';
import type { Deferred } from 'typed-core/dist/promises/deferred';
import type { HeaderInterpreter } from '../header/types';
import type { HeadersInterpreter } from '../header/types';
import type { AxiosInterceptor } from '../interceptors/types';
import type { CachedResponse, CacheStorage } from '../storage/types';
import type { AxiosStorage } from '../storage/storage';
import type { CachedResponse } from '../storage/types';
import type { CachePredicate, KeyGenerator } from '../util/types';

@@ -57,5 +58,24 @@ import type { CacheUpdater } from '../util/update-cache';

*
* @default
* @default {{}}
*/
update: Record<string, CacheUpdater>;
/**
* If the request should handle ETag and If-None-Match support. Use
* a string to force a custom value or true to use the response ETag
*
* @default false
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
*/
etag: string | boolean;
/**
* Use If-Modified-Since header in this request. Use a date to force
* a custom value or true to use the last cached timestamp. If never
* cached before, the header is not set.
*
* @default false
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
*/
modifiedSince: Date | boolean;
};

@@ -67,5 +87,5 @@

*
* @default new MemoryStorage()
* @default new MemoryAxiosStorage()
*/
storage: CacheStorage;
storage: AxiosStorage;

@@ -92,3 +112,3 @@ /**

*/
headerInterpreter: HeaderInterpreter;
headerInterpreter: HeadersInterpreter;

@@ -95,0 +115,0 @@ /**

@@ -5,3 +5,3 @@ import Axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

import { CacheResponseInterceptor } from '../interceptors/response';
import { MemoryStorage } from '../storage/memory';
import { MemoryAxiosStorage } from '../storage/memory';
import { defaultKeyGenerator } from '../util/key-generator';

@@ -32,3 +32,3 @@ import type { AxiosCacheInstance } from './axios';

axiosCache.storage = storage || new MemoryStorage();
axiosCache.storage = storage || new MemoryAxiosStorage({});
axiosCache.generateKey = generateKey || defaultKeyGenerator;

@@ -49,5 +49,5 @@ axiosCache.waiting = waiting || {};

methods: ['get'],
cachePredicate: {
statusCheck: [200, 399]
},
cachePredicate: { statusCheck: [200, 399] },
etag: false,
modifiedSince: false,
update: {},

@@ -54,0 +54,0 @@ ...cacheOptions

import { parse } from '@tusbar/cache-control';
import type { HeaderInterpreter } from './types';
import { Header } from '../util/headers';
import type { HeaderInterpreter, HeadersInterpreter } from './types';
export const defaultHeaderInterpreter: HeaderInterpreter = (headers) => {
const cacheControl = headers?.['cache-control'];
export const defaultHeaderInterpreter: HeadersInterpreter = (headers = {}) => {
if (Header.CacheControl in headers) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return interpretCacheControl(headers[Header.CacheControl]!, headers);
}
if (!cacheControl) {
// Checks if Expires header is present
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires
const expires = headers?.['expires'];
if (Header.Expires in headers) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return interpretExpires(headers[Header.Expires]!, headers);
}
if (expires) {
const milliseconds = Date.parse(expires) - Date.now();
return undefined;
};
if (milliseconds > 0) {
return milliseconds;
} else {
return false;
}
}
const interpretExpires: HeaderInterpreter = (expires) => {
const milliseconds = Date.parse(expires) - Date.now();
return milliseconds >= 0 ? milliseconds : false;
};
return undefined;
}
const interpretCacheControl: HeaderInterpreter = (cacheControl, headers) => {
const { noCache, noStore, mustRevalidate, maxAge, immutable } = parse(cacheControl);
const { noCache, noStore, mustRevalidate, maxAge } = parse(cacheControl);
// Header told that this response should not be cached.
if (noCache || noStore || mustRevalidate) {
if (noCache || noStore) {
return false;
}
if (!maxAge) {
return undefined;
if (immutable) {
// 1 year is sufficient, as Infinity may cause more problems.
// It might not be the best way, but a year is better than none.
return 1000 * 60 * 60 * 24 * 365;
}
return maxAge * 1000;
// Already out of date, for cache can be saved, but must be requested again
if (mustRevalidate) {
return 0;
}
if (maxAge) {
const age = headers[Header.Age];
if (!age) {
return maxAge * 1000;
}
return maxAge * 1000 - Number(age) * 1000;
}
return undefined;
};
/**
* Interpret the cache control header, if present.
* `false` if cache should not be used.
*
* `undefined` when provided headers was not enough to determine a valid value.
*
* `number` containing the number of **milliseconds** to cache the response.
*/
type MaybeTtl = false | undefined | number;
/**
* Interpret all http headers to determina a time to live.
*
* @param header The header object to interpret.

@@ -9,4 +18,15 @@ * @returns `false` if cache should not be used. `undefined` when

*/
export type HeadersInterpreter = (headers?: Record<string, string>) => MaybeTtl;
/**
* Interpret a single string header
*
* @param header The header string to interpret.
* @returns `false` if cache should not be used. `undefined` when
* provided headers was not enough to determine a valid value. Or a
* `number` containing the number of **milliseconds** to cache the response.
*/
export type HeaderInterpreter = (
headers?: Record<Lowercase<string>, string>
) => false | undefined | number;
header: string,
headers: Record<string, string>
) => MaybeTtl;

@@ -6,3 +6,4 @@ export * from './cache/axios';

export * from './interceptors/types';
export * from './storage/storage';
export * from './storage/types';
export * from './util/types';

@@ -0,2 +1,4 @@

import type { AxiosRequestConfig, Method } from 'axios';
import { deferred } from 'typed-core/dist/promises/deferred';
import type { CacheProperties } from '..';
import type {

@@ -10,4 +12,6 @@ AxiosCacheInstance,

CachedStorageValue,
LoadingStorageValue
LoadingStorageValue,
StaleStorageValue
} from '../storage/types';
import { Header } from '../util/headers';
import type { AxiosInterceptor } from './types';

@@ -20,8 +24,9 @@

use = (): void => {
public use = (): void => {
this.axios.interceptors.request.use(this.onFulfilled);
};
onFulfilled = async (config: CacheRequestConfig<D>): Promise<CacheRequestConfig<D>> => {
// Skip cache
public onFulfilled = async (
config: CacheRequestConfig<D>
): Promise<CacheRequestConfig<D>> => {
if (config.cache === false) {

@@ -31,7 +36,8 @@ return config;

// Only cache specified methods
const allowedMethods = config.cache?.methods || this.axios.defaults.cache.methods;
// merge defaults with per request configuration
config.cache = { ...this.axios.defaults.cache, ...config.cache };
if (
!allowedMethods.some((method) => (config.method || 'get').toLowerCase() == method)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
!this.isMethodAllowed(config.method!, config.cache)
) {

@@ -47,3 +53,3 @@ return config;

// Not cached, continue the request, and mark it as fetching
emptyState: if (cache.state == 'empty') {
emptyOrStale: if (cache.state == 'empty' || cache.state === 'stale') {
/**

@@ -58,3 +64,3 @@ * This checks for simultaneous access to a new key. The js

| LoadingStorageValue;
break emptyState;
break emptyOrStale;
}

@@ -73,5 +79,14 @@

state: 'loading',
ttl: config.cache?.ttl
data: cache.data
});
if (cache.state === 'stale') {
//@ts-expect-error type infer couldn't resolve this
this.setRevalidationHeaders(cache, config);
}
config.validateStatus = CacheRequestInterceptor.createValidateStatus(
config.validateStatus
);
return config;

@@ -85,6 +100,4 @@ }

/**
* If the deferred is undefined, means that the outside has
* removed that key from the waiting list
*/
// Just in case, the deferred doesn't exists.
/* istanbul ignore if 'really hard to test' */
if (!deferred) {

@@ -122,2 +135,54 @@ await this.axios.storage.remove(key);

};
private isMethodAllowed = (
method: Method,
properties: Partial<CacheProperties>
): boolean => {
const requestMethod = method.toLowerCase();
for (const method of properties.methods || []) {
if (method.toLowerCase() === requestMethod) {
return true;
}
}
return false;
};
private setRevalidationHeaders = (
cache: StaleStorageValue,
config: CacheRequestConfig<D> & { cache: Partial<CacheProperties> }
): void => {
config.headers ||= {};
const { etag, modifiedSince } = config.cache;
if (etag) {
const etagValue = etag === true ? cache.data?.headers[Header.ETag] : etag;
if (etagValue) {
config.headers[Header.IfNoneMatch] = etagValue;
}
}
if (modifiedSince) {
config.headers[Header.IfModifiedSince] =
modifiedSince === true
? // If last-modified is not present, use the createdAt timestamp
cache.data.headers[Header.LastModified] ||
new Date(cache.createdAt).toUTCString()
: modifiedSince.toUTCString();
}
};
/**
* Creates a new validateStatus function that will use the one
* already used and also accept status code 304.
*/
static createValidateStatus = (oldValidate?: AxiosRequestConfig['validateStatus']) => {
return (status: number): boolean => {
return oldValidate
? oldValidate(status) || status === 304
: (status >= 200 && status < 300) || status === 304;
};
};
}

@@ -7,2 +7,3 @@ import type { AxiosResponse } from 'axios';

import { checkPredicateObject } from '../util/cache-predicate';
import { Header } from '../util/headers';
import { updateCache } from '../util/update-cache';

@@ -16,66 +17,33 @@ import type { AxiosInterceptor } from './types';

use = (): void => {
public use = (): void => {
this.axios.interceptors.response.use(this.onFulfilled);
};
private testCachePredicate = <R>(
response: AxiosResponse<R>,
cache?: Partial<CacheProperties>
): boolean => {
const cachePredicate =
cache?.cachePredicate || this.axios.defaults.cache.cachePredicate;
return (
(typeof cachePredicate === 'function' && cachePredicate(response)) ||
(typeof cachePredicate === 'object' &&
checkPredicateObject(response, cachePredicate))
);
};
/**
* Rejects cache for this response. Also update the waiting list for
* this key by rejecting it.
*/
private rejectResponse = async (key: string) => {
// Update the cache to empty to prevent infinite loading state
await this.axios.storage.remove(key);
// Reject the deferred if present
this.axios.waiting[key]?.reject();
delete this.axios.waiting[key];
};
onFulfilled = async (
public onFulfilled = async (
axiosResponse: AxiosResponse<R, D>
): Promise<CacheAxiosResponse<R, D>> => {
const key = this.axios.generateKey(axiosResponse.config);
const response = this.cachedResponse(axiosResponse);
const response: CacheAxiosResponse<R, D> = {
id: key,
// Response is already cached
if (response.cached) {
return response;
}
/**
* The request interceptor response.cache will return true or
* undefined. And true only when the response was cached.
*/
cached: (axiosResponse as CacheAxiosResponse<R, D>).cached || false,
...axiosResponse
};
// Skip cache
if (response.config.cache === false) {
// either false or weird behavior, config.cache should always exists, from global config merge at least
if (!response.config.cache) {
return { ...response, cached: false };
}
// Response is already cached
if (response.cached) {
return response;
}
const cacheConfig = response.config.cache as CacheProperties;
const cache = await this.axios.storage.get(key);
const cache = await this.axios.storage.get(response.id);
/**
* From now on, the cache and response represents the state of the
* first response to a request, which has not yet been cached or
* processed before.
*/
if (cache.state !== 'loading') {
if (
// If the request interceptor had a problem
cache.state === 'stale' ||
cache.state === 'empty' ||
// Should not hit here because of later response.cached check
cache.state === 'cached'
) {
return response;

@@ -85,10 +53,29 @@ }

// Config told that this response should be cached.
if (!this.testCachePredicate(response, response.config.cache)) {
await this.rejectResponse(key);
if (
// For 'loading' values (post stale), this check was already run in the past.
!cache.data &&
!this.testCachePredicate(response, cacheConfig)
) {
await this.rejectResponse(response.id);
return response;
}
let ttl = response.config.cache?.ttl || this.axios.defaults.cache.ttl;
// avoid remnant headers from remote server to break implementation
delete response.headers[Header.XAxiosCacheEtag];
delete response.headers[Header.XAxiosCacheLastModified];
if (response.config.cache?.interpretHeader) {
if (cacheConfig.etag && cacheConfig.etag !== true) {
response.headers[Header.XAxiosCacheEtag] = cacheConfig.etag;
}
if (cacheConfig.modifiedSince) {
response.headers[Header.XAxiosCacheLastModified] =
cacheConfig.modifiedSince === true
? 'use-cache-timestamp'
: cacheConfig.modifiedSince.toUTCString();
}
let ttl = cacheConfig.ttl || -1; // always set from global config
if (cacheConfig?.interpretHeader) {
const expirationTime = this.axios.headerInterpreter(response.headers);

@@ -98,29 +85,48 @@

if (expirationTime === false) {
await this.rejectResponse(key);
await this.rejectResponse(response.id);
return response;
}
ttl = expirationTime ? expirationTime : ttl;
ttl = expirationTime || expirationTime === 0 ? expirationTime : ttl;
}
const data =
response.status == 304 && cache.data
? (() => {
// Rust syntax <3
response.cached = true;
response.data = cache.data.data;
response.status = cache.data.status;
response.statusText = cache.data.statusText;
// We may have new headers.
response.headers = {
...cache.data.headers,
...response.headers
};
return cache.data;
})()
: extract(response, ['data', 'headers', 'status', 'statusText']);
const newCache: CachedStorageValue = {
state: 'cached',
ttl: ttl,
ttl,
createdAt: Date.now(),
data: extract(response, ['data', 'headers', 'status', 'statusText'])
data
};
// Update other entries before updating himself
if (response.config.cache?.update) {
updateCache(this.axios.storage, response.data, response.config.cache.update);
if (cacheConfig?.update) {
updateCache(this.axios.storage, response.data, cacheConfig.update);
}
const deferred = this.axios.waiting[key];
const deferred = this.axios.waiting[response.id];
// Resolve all other requests waiting for this response
await deferred?.resolve(newCache.data);
delete this.axios.waiting[key];
delete this.axios.waiting[response.id];
// Define this key as cache on the storage
await this.axios.storage.set(key, newCache);
await this.axios.storage.set(response.id, newCache);

@@ -130,2 +136,39 @@ // Return the response with cached as false, because it was not cached at all

};
private testCachePredicate = <R>(
response: AxiosResponse<R>,
cache: CacheProperties
): boolean => {
const cachePredicate = cache.cachePredicate;
return (
(typeof cachePredicate === 'function' && cachePredicate(response)) ||
(typeof cachePredicate === 'object' &&
checkPredicateObject(response, cachePredicate))
);
};
/**
* Rejects cache for this response. Also update the waiting list for
* this key by rejecting it.
*/
private rejectResponse = async (key: string) => {
// Update the cache to empty to prevent infinite loading state
await this.axios.storage.remove(key);
// Reject the deferred if present
this.axios.waiting[key]?.reject();
delete this.axios.waiting[key];
};
private cachedResponse = (response: AxiosResponse<R, D>): CacheAxiosResponse<R, D> => {
return {
id: this.axios.generateKey(response.config),
/**
* The request interceptor response.cache will return true or
* undefined. And true only when the response was cached.
*/
cached: (response as CacheAxiosResponse<R, D>).cached || false,
...response
};
};
}

@@ -1,29 +0,20 @@

import type { CacheStorage, StorageValue } from './types';
import { isCacheValid } from './util';
import { AxiosStorage } from './storage';
import type { NotEmptyStorageValue, StorageValue } from './types';
export class MemoryStorage implements CacheStorage {
private readonly storage: Map<string, StorageValue> = new Map();
export class MemoryAxiosStorage extends AxiosStorage {
constructor(readonly storage: Record<string, StorageValue> = {}) {
super();
}
get = async (key: string): Promise<StorageValue> => {
const value = this.storage.get(key);
if (!value) {
return { state: 'empty' };
}
if (isCacheValid(value) === false) {
this.remove(key);
return { state: 'empty' };
}
return value;
public find = async (key: string): Promise<StorageValue> => {
return this.storage[key] || { state: 'empty' };
};
set = async (key: string, value: StorageValue): Promise<void> => {
this.storage.set(key, value);
public set = async (key: string, value: NotEmptyStorageValue): Promise<void> => {
this.storage[key] = value;
};
remove = async (key: string): Promise<void> => {
this.storage.delete(key);
public remove = async (key: string): Promise<void> => {
delete this.storage[key];
};
}

@@ -1,21 +0,1 @@

export interface CacheStorage {
/**
* Returns the cached value for the given key. Must handle cache
* miss and staling by returning a new `StorageValue` with `empty` state.
*/
get: (key: string) => Promise<StorageValue>;
/**
* Sets a new value for the given key
*
* Use CacheStorage.remove(key) to define a key to 'empty' state.
*/
set: (key: string, value: LoadingStorageValue | CachedStorageValue) => Promise<void>;
/**
* Removes the value for the given key
*/
remove: (key: string) => Promise<void>;
}
export type CachedResponse = {

@@ -31,4 +11,17 @@ data?: any;

*/
export type StorageValue = CachedStorageValue | LoadingStorageValue | EmptyStorageValue;
export type StorageValue =
| StaleStorageValue
| CachedStorageValue
| LoadingStorageValue
| EmptyStorageValue;
export type NotEmptyStorageValue = Exclude<StorageValue, EmptyStorageValue>;
export type StaleStorageValue = {
data: CachedResponse;
ttl?: undefined;
createdAt: number;
state: 'stale';
};
export type CachedStorageValue = {

@@ -46,3 +39,7 @@ data: CachedResponse;

export type LoadingStorageValue = {
data?: undefined;
/**
* Only present if the previous state was `stale`. So, in case the
* new response comes without a value, this data is used
*/
data?: CachedResponse;
ttl?: number;

@@ -49,0 +46,0 @@

@@ -0,5 +1,6 @@

import type { AxiosStorage } from '../storage/storage';
import type {
CachedStorageValue,
CacheStorage,
EmptyStorageValue
LoadingStorageValue,
StorageValue
} from '../storage/types';

@@ -10,8 +11,12 @@

| ((
cached: EmptyStorageValue | CachedStorageValue,
cached: Exclude<StorageValue, LoadingStorageValue>,
newData: any
) => CachedStorageValue | void);
/**
* Function to update all caches, from CacheProperties.update, with
* the new data.
*/
export async function updateCache<T = any>(
storage: CacheStorage,
storage: AxiosStorage,
data: T,

@@ -24,3 +29,3 @@ entries: Record<string, CacheUpdater>

if (value == 'delete') {
if (value === 'delete') {
await storage.remove(cacheKey);

@@ -27,0 +32,0 @@ continue;

@@ -14,3 +14,3 @@ {

/* Language and Environment */
"target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */

@@ -17,0 +17,0 @@ // "jsx": "preserve", /* Specify what JSX code is generated. */

@@ -53,3 +53,3 @@ import type { AxiosDefaults, AxiosInstance, AxiosInterceptorManager, AxiosRequestConfig, AxiosResponse } from 'axios';

*/
<T = any, D = any, R = CacheAxiosResponse<T, D>>(config?: CacheRequestConfig<D>): Promise<R>;
<T = any, D = any, R = CacheAxiosResponse<T, D>>(config: CacheRequestConfig<D>): Promise<R>;
/**

@@ -56,0 +56,0 @@ * @template T The type returned by this response

import type { Method } from 'axios';
import type { Deferred } from 'typed-core/dist/promises/deferred';
import type { HeaderInterpreter } from '../header/types';
import type { HeadersInterpreter } from '../header/types';
import type { AxiosInterceptor } from '../interceptors/types';
import type { CachedResponse, CacheStorage } from '../storage/types';
import type { AxiosStorage } from '../storage/storage';
import type { CachedResponse } from '../storage/types';
import type { CachePredicate, KeyGenerator } from '../util/types';

@@ -52,5 +53,22 @@ import type { CacheUpdater } from '../util/update-cache';

*
* @default
* @default {{}}
*/
update: Record<string, CacheUpdater>;
/**
* If the request should handle ETag and If-None-Match support. Use
* a string to force a custom value or true to use the response ETag
*
* @default false
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
*/
etag: string | boolean;
/**
* Use If-Modified-Since header in this request. Use a date to force
* a custom value or true to use the last cached timestamp. If never
* cached before, the header is not set.
*
* @default false
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
*/
modifiedSince: Date | boolean;
};

@@ -61,5 +79,5 @@ export interface CacheInstance {

*
* @default new MemoryStorage()
* @default new MemoryAxiosStorage()
*/
storage: CacheStorage;
storage: AxiosStorage;
/**

@@ -83,3 +101,3 @@ * The function used to create different keys for each request.

*/
headerInterpreter: HeaderInterpreter;
headerInterpreter: HeadersInterpreter;
/**

@@ -86,0 +104,0 @@ * The request interceptor that will be used to handle the cache.

@@ -1,3 +0,3 @@

import type { HeaderInterpreter } from './types';
export declare const defaultHeaderInterpreter: HeaderInterpreter;
import type { HeadersInterpreter } from './types';
export declare const defaultHeaderInterpreter: HeadersInterpreter;
//# sourceMappingURL=interpreter.d.ts.map
/**
* Interpret the cache control header, if present.
* `false` if cache should not be used.
*
* `undefined` when provided headers was not enough to determine a valid value.
*
* `number` containing the number of **milliseconds** to cache the response.
*/
declare type MaybeTtl = false | undefined | number;
/**
* Interpret all http headers to determina a time to live.
*
* @param header The header object to interpret.

@@ -9,3 +17,13 @@ * @returns `false` if cache should not be used. `undefined` when

*/
export declare type HeaderInterpreter = (headers?: Record<Lowercase<string>, string>) => false | undefined | number;
export declare type HeadersInterpreter = (headers?: Record<string, string>) => MaybeTtl;
/**
* Interpret a single string header
*
* @param header The header string to interpret.
* @returns `false` if cache should not be used. `undefined` when
* provided headers was not enough to determine a valid value. Or a
* `number` containing the number of **milliseconds** to cache the response.
*/
export declare type HeaderInterpreter = (header: string, headers: Record<string, string>) => MaybeTtl;
export {};
//# sourceMappingURL=types.d.ts.map

@@ -6,4 +6,5 @@ export * from './cache/axios';

export * from './interceptors/types';
export * from './storage/storage';
export * from './storage/types';
export * from './util/types';
//# sourceMappingURL=index.d.ts.map

@@ -0,1 +1,2 @@

import type { AxiosRequestConfig } from 'axios';
import type { AxiosCacheInstance, CacheRequestConfig } from '../cache/axios';

@@ -8,3 +9,10 @@ import type { AxiosInterceptor } from './types';

onFulfilled: (config: CacheRequestConfig<D>) => Promise<CacheRequestConfig<D>>;
private isMethodAllowed;
private setRevalidationHeaders;
/**
* Creates a new validateStatus function that will use the one
* already used and also accept status code 304.
*/
static createValidateStatus: (oldValidate?: AxiosRequestConfig['validateStatus']) => (status: number) => boolean;
}
//# sourceMappingURL=request.d.ts.map

@@ -8,2 +8,3 @@ import type { AxiosResponse } from 'axios';

use: () => void;
onFulfilled: (axiosResponse: AxiosResponse<R, D>) => Promise<CacheAxiosResponse<R, D>>;
private testCachePredicate;

@@ -15,4 +16,4 @@ /**

private rejectResponse;
onFulfilled: (axiosResponse: AxiosResponse<R, D>) => Promise<CacheAxiosResponse<R, D>>;
private cachedResponse;
}
//# sourceMappingURL=response.d.ts.map

@@ -1,8 +0,10 @@

import type { CacheStorage, StorageValue } from './types';
export declare class MemoryStorage implements CacheStorage {
private readonly storage;
get: (key: string) => Promise<StorageValue>;
set: (key: string, value: StorageValue) => Promise<void>;
import { AxiosStorage } from './storage';
import type { NotEmptyStorageValue, StorageValue } from './types';
export declare class MemoryAxiosStorage extends AxiosStorage {
readonly storage: Record<string, StorageValue>;
constructor(storage?: Record<string, StorageValue>);
find: (key: string) => Promise<StorageValue>;
set: (key: string, value: NotEmptyStorageValue) => Promise<void>;
remove: (key: string) => Promise<void>;
}
//# sourceMappingURL=memory.d.ts.map

@@ -1,18 +0,1 @@

export interface CacheStorage {
/**
* Returns the cached value for the given key. Must handle cache
* miss and staling by returning a new `StorageValue` with `empty` state.
*/
get: (key: string) => Promise<StorageValue>;
/**
* Sets a new value for the given key
*
* Use CacheStorage.remove(key) to define a key to 'empty' state.
*/
set: (key: string, value: LoadingStorageValue | CachedStorageValue) => Promise<void>;
/**
* Removes the value for the given key
*/
remove: (key: string) => Promise<void>;
}
export declare type CachedResponse = {

@@ -27,3 +10,10 @@ data?: any;

*/
export declare type StorageValue = CachedStorageValue | LoadingStorageValue | EmptyStorageValue;
export declare type StorageValue = StaleStorageValue | CachedStorageValue | LoadingStorageValue | EmptyStorageValue;
export declare type NotEmptyStorageValue = Exclude<StorageValue, EmptyStorageValue>;
export declare type StaleStorageValue = {
data: CachedResponse;
ttl?: undefined;
createdAt: number;
state: 'stale';
};
export declare type CachedStorageValue = {

@@ -40,3 +30,7 @@ data: CachedResponse;

export declare type LoadingStorageValue = {
data?: undefined;
/**
* Only present if the previous state was `stale`. So, in case the
* new response comes without a value, this data is used
*/
data?: CachedResponse;
ttl?: number;

@@ -43,0 +37,0 @@ /**

@@ -1,4 +0,9 @@

import type { CachedStorageValue, CacheStorage, EmptyStorageValue } from '../storage/types';
export declare type CacheUpdater = 'delete' | ((cached: EmptyStorageValue | CachedStorageValue, newData: any) => CachedStorageValue | void);
export declare function updateCache<T = any>(storage: CacheStorage, data: T, entries: Record<string, CacheUpdater>): Promise<void>;
import type { AxiosStorage } from '../storage/storage';
import type { CachedStorageValue, LoadingStorageValue, StorageValue } from '../storage/types';
export declare type CacheUpdater = 'delete' | ((cached: Exclude<StorageValue, LoadingStorageValue>, newData: any) => CachedStorageValue | void);
/**
* Function to update all caches, from CacheProperties.update, with
* the new data.
*/
export declare function updateCache<T = any>(storage: AxiosStorage, data: T, entries: Record<string, CacheUpdater>): Promise<void>;
//# sourceMappingURL=update-cache.d.ts.map

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

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