axios-cache-interceptor
Advanced tools
Comparing version 0.0.3 to 0.0.5
import { AxiosInstance } from 'axios'; | ||
import { AxiosCacheInstance, CacheInstance, CacheRequestConfig } from './types'; | ||
declare type Options = CacheRequestConfig['cache'] & Partial<CacheInstance>; | ||
export declare function createCache(axios: AxiosInstance, options?: Options): AxiosCacheInstance; | ||
export {}; | ||
import CacheInstance, { AxiosCacheInstance, CacheProperties } from './types'; | ||
export declare function createCache(axios: AxiosInstance, options?: Partial<CacheInstance & CacheProperties>): AxiosCacheInstance; | ||
//# sourceMappingURL=cache.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createCache = void 0; | ||
const header_1 = require("../header"); | ||
const request_1 = require("../interceptors/request"); | ||
const response_1 = require("../interceptors/response"); | ||
const memory_1 = require("../storage/memory"); | ||
const key_generator_1 = require("../utils/key-generator"); | ||
const key_generator_1 = require("../util/key-generator"); | ||
function createCache(axios, options = {}) { | ||
const axiosCache = axios; | ||
axiosCache.storage = options.storage || new memory_1.MemoryStorage(); | ||
axiosCache.generateKey = key_generator_1.defaultKeyGenerator; | ||
axiosCache.generateKey = options.generateKey || key_generator_1.defaultKeyGenerator; | ||
axiosCache.waiting = options.waiting || {}; | ||
axiosCache.interpretHeader = options.interpretHeader || header_1.defaultHeaderInterpreter; | ||
// CacheRequestConfig values | ||
axiosCache.defaults = Object.assign(Object.assign({}, axios.defaults), { cache: Object.assign({ maxAge: 1000 * 60 * 5, interpretHeader: false, methods: ['get'], shouldCache: ({ status }) => status >= 200 && status < 300, update: {} }, options) }); | ||
axiosCache.defaults = { | ||
...axios.defaults, | ||
cache: { | ||
maxAge: 1000 * 60 * 5, | ||
interpretHeader: false, | ||
methods: ['get'], | ||
cachePredicate: ({ status }) => status >= 200 && status < 300, | ||
update: {}, | ||
...options | ||
} | ||
}; | ||
// Apply interceptors | ||
@@ -15,0 +28,0 @@ (0, request_1.applyRequestInterceptor)(axiosCache); |
import type { AxiosInstance, AxiosInterceptorManager, AxiosPromise, AxiosRequestConfig, AxiosResponse, Method } from 'axios'; | ||
import { CacheStorage } from '../storage/types'; | ||
import { Deferred } from 'src/util/deferred'; | ||
import { KeyGenerator } from 'src/util/key-generator'; | ||
import { HeaderInterpreter } from '../header'; | ||
import { CachedResponse, CacheStorage } from '../storage/types'; | ||
import { CachePredicate } from '../util/cache-predicate'; | ||
export declare type DefaultCacheRequestConfig = AxiosRequestConfig & { | ||
cache: CacheProperties; | ||
}; | ||
export declare type CacheProperties = { | ||
/** | ||
* The time until the cached value is expired in milliseconds. | ||
* | ||
* @default 1000 * 60 * 5 | ||
*/ | ||
maxAge: number; | ||
/** | ||
* If this interceptor should configure the cache from the request cache header | ||
* When used, the maxAge property is ignored | ||
* | ||
* @default false | ||
*/ | ||
interpretHeader: boolean; | ||
/** | ||
* All methods that should be cached. | ||
* | ||
* @default ['get'] | ||
*/ | ||
methods: Lowercase<Method>[]; | ||
/** | ||
* The function to check if the response code permit being cached. | ||
* | ||
* @default ({ status }) => status >= 200 && status < 300 | ||
*/ | ||
cachePredicate: CachePredicate; | ||
/** | ||
* Once the request is resolved, this specifies what requests should we change the cache. | ||
* Can be used to update the request or delete other caches. | ||
* | ||
* If the function returns void, the entry is deleted | ||
* | ||
* This is independent if the request made was cached or not. | ||
* | ||
* The id used is the same as the id on `CacheRequestConfig['id']`, auto-generated or not. | ||
* | ||
* @default {} | ||
*/ | ||
update: { | ||
[id: string]: 'delete' | ((oldValue: any, atual: any) => any | undefined); | ||
}; | ||
}; | ||
/** | ||
@@ -17,46 +66,5 @@ * Options that can be overridden per request | ||
*/ | ||
cache?: { | ||
/** | ||
* The time until the cached value is expired in milliseconds. | ||
* | ||
* @default 1000 * 60 * 5 | ||
*/ | ||
maxAge?: number; | ||
/** | ||
* If this interceptor should configure the cache from the request cache header | ||
* When used, the maxAge property is ignored | ||
* | ||
* @default false | ||
*/ | ||
interpretHeader?: boolean; | ||
/** | ||
* All methods that should be cached. | ||
* | ||
* @default ['get'] | ||
*/ | ||
methods?: Lowercase<Method>[]; | ||
/** | ||
* The function to check if the response code permit being cached. | ||
* | ||
* @default ({ status }) => status >= 200 && status < 300 | ||
*/ | ||
shouldCache?: (response: AxiosResponse) => boolean; | ||
/** | ||
* Once the request is resolved, this specifies what requests should we change the cache. | ||
* Can be used to update the request or delete other caches. | ||
* | ||
* If the function returns void, the entry is deleted | ||
* | ||
* This is independent if the request made was cached or not. | ||
* | ||
* The id used is the same as the id on `CacheRequestConfig['id']`, auto-generated or not. | ||
* | ||
* @default {} | ||
*/ | ||
update?: { | ||
[id: string]: 'delete' | ((oldValue: any, atual: any) => any | void); | ||
}; | ||
}; | ||
cache?: Partial<CacheProperties>; | ||
}; | ||
export interface CacheInstance { | ||
export default interface CacheInstance { | ||
/** | ||
@@ -73,8 +81,24 @@ * The storage to save the cache data. | ||
*/ | ||
generateKey: (options: CacheRequestConfig) => string; | ||
generateKey: KeyGenerator; | ||
/** | ||
* A simple object that holds all deferred objects until it is resolved. | ||
*/ | ||
waiting: Record<string, Deferred<CachedResponse>>; | ||
/** | ||
* The function to parse and interpret response headers. | ||
* Only used if cache.interpretHeader is true. | ||
*/ | ||
interpretHeader: HeaderInterpreter; | ||
} | ||
/** | ||
* Same as the AxiosInstance but with CacheRequestConfig as a config type. | ||
* | ||
* @see AxiosInstance | ||
* @see CacheRequestConfig | ||
* @see CacheInstance | ||
*/ | ||
export interface AxiosCacheInstance extends AxiosInstance, CacheInstance { | ||
(config: CacheRequestConfig): AxiosPromise; | ||
(url: string, config?: CacheRequestConfig): AxiosPromise; | ||
defaults: CacheRequestConfig; | ||
defaults: DefaultCacheRequestConfig; | ||
interceptors: { | ||
@@ -81,0 +105,0 @@ request: AxiosInterceptorManager<CacheRequestConfig>; |
@@ -1,4 +0,4 @@ | ||
export { createCache } from './axios/cache'; | ||
export * from './constants'; | ||
export * from './axios'; | ||
export * from './storage'; | ||
export * as StatusCodes from './util/status-codes'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -9,11 +9,22 @@ "use strict"; | ||
})); | ||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
}) : function(o, v) { | ||
o["default"] = v; | ||
}); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
var __importStar = (this && this.__importStar) || function (mod) { | ||
if (mod && mod.__esModule) return mod; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
return result; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createCache = void 0; | ||
var cache_1 = require("./axios/cache"); | ||
Object.defineProperty(exports, "createCache", { enumerable: true, get: function () { return cache_1.createCache; } }); | ||
__exportStar(require("./constants"), exports); | ||
exports.StatusCodes = void 0; | ||
__exportStar(require("./axios"), exports); | ||
__exportStar(require("./storage"), exports); | ||
exports.StatusCodes = __importStar(require("./util/status-codes")); | ||
//# sourceMappingURL=index.js.map |
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.applyRequestInterceptor = void 0; | ||
const constants_1 = require("../constants"); | ||
const deferred_1 = require("../utils/deferred"); | ||
const deferred_1 = require("../util/deferred"); | ||
const status_codes_1 = require("../util/status-codes"); | ||
function applyRequestInterceptor(axios) { | ||
axios.interceptors.request.use((config) => __awaiter(this, void 0, void 0, function* () { | ||
var _a, _b, _c, _d; | ||
axios.interceptors.request.use(async (config) => { | ||
// Only cache specified methods | ||
if ((_b = (_a = config.cache) === null || _a === void 0 ? void 0 : _a.methods) === null || _b === void 0 ? void 0 : _b.some((method) => (config.method || 'get').toLowerCase() == method)) { | ||
if (config.cache?.methods?.some((method) => (config.method || 'get').toLowerCase() == method)) { | ||
return config; | ||
} | ||
const key = axios.generateKey(config); | ||
const cache = yield axios.storage.get(key); | ||
const cache = await axios.storage.get(key); | ||
// Not cached, continue the request, and mark it as fetching | ||
if (cache.state == 'empty') { | ||
yield axios.storage.set(key, { | ||
// Create a deferred to resolve other requests for the same key when it's completed | ||
axios.waiting[key] = new deferred_1.Deferred(); | ||
await axios.storage.set(key, { | ||
state: 'loading', | ||
data: new deferred_1.Deferred(), | ||
data: null, | ||
// The cache header will be set after the response has been read, until that time, the expiration will be -1 | ||
expiration: ((_c = config.cache) === null || _c === void 0 ? void 0 : _c.interpretHeader) | ||
expiration: config.cache?.interpretHeader | ||
? -1 | ||
: ((_d = config.cache) === null || _d === void 0 ? void 0 : _d.maxAge) || axios.defaults.cache.maxAge | ||
: config.cache?.maxAge || axios.defaults.cache.maxAge | ||
}); | ||
@@ -38,17 +30,29 @@ return config; | ||
if (cache.state === 'cached' && cache.expiration < Date.now()) { | ||
yield axios.storage.remove(key); | ||
await axios.storage.remove(key); | ||
return config; | ||
} | ||
const { body, headers } = yield cache.data; | ||
let data = {}; | ||
if (cache.state === 'loading') { | ||
const deferred = axios.waiting[key]; | ||
// If the deferred is undefined, means that the | ||
// outside has removed that key from the waiting list | ||
if (!deferred) { | ||
return config; | ||
} | ||
data = await deferred; | ||
} | ||
else { | ||
data = cache.data; | ||
} | ||
config.adapter = () => Promise.resolve({ | ||
data: body, | ||
config, | ||
headers, | ||
status: constants_1.CACHED_RESPONSE_STATUS, | ||
statusText: constants_1.CACHED_RESPONSE_STATUS_TEXT | ||
data: data.body, | ||
headers: data.headers, | ||
status: status_codes_1.CACHED_RESPONSE_STATUS, | ||
statusText: status_codes_1.CACHED_RESPONSE_STATUS_TEXT | ||
}); | ||
return config; | ||
})); | ||
}); | ||
} | ||
exports.applyRequestInterceptor = applyRequestInterceptor; | ||
//# sourceMappingURL=request.js.map |
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.applyResponseInterceptor = void 0; | ||
const cache_control_1 = require("@tusbar/cache-control"); | ||
const cache_predicate_1 = require("../util/cache-predicate"); | ||
const update_cache_1 = require("../util/update-cache"); | ||
function applyResponseInterceptor(axios) { | ||
axios.interceptors.response.use((response) => __awaiter(this, void 0, void 0, function* () { | ||
var _a, _b, _c, _d, _e; | ||
axios.interceptors.response.use(async (response) => { | ||
// Update other entries before updating himself | ||
for (const [cacheKey, value] of Object.entries(((_a = response.config.cache) === null || _a === void 0 ? void 0 : _a.update) || {})) { | ||
if (value == 'delete') { | ||
yield axios.storage.remove(cacheKey); | ||
continue; | ||
if (response.config.cache?.update) { | ||
(0, update_cache_1.updateCache)(axios, response.data, response.config.cache.update); | ||
} | ||
const cachePredicate = response.config.cache?.cachePredicate || axios.defaults.cache.cachePredicate; | ||
// Config told that this response should be cached. | ||
if (typeof cachePredicate === 'function') { | ||
if (!cachePredicate(response)) { | ||
return response; | ||
} | ||
const oldValue = yield axios.storage.get(cacheKey); | ||
const newValue = value(oldValue, response.data); | ||
if (newValue !== undefined) { | ||
yield axios.storage.set(cacheKey, newValue); | ||
} | ||
else { | ||
if (!(0, cache_predicate_1.checkPredicateObject)(response, cachePredicate)) { | ||
return response; | ||
} | ||
else { | ||
yield axios.storage.remove(cacheKey); | ||
} | ||
} | ||
// Config told that this response should be cached. | ||
if (!((_b = response.config.cache) === null || _b === void 0 ? void 0 : _b.shouldCache(response))) { | ||
return response; | ||
} | ||
const key = axios.generateKey(response.config); | ||
const cache = yield axios.storage.get(key); | ||
if ( | ||
// Response already is in cache. | ||
cache.state === 'cached' || | ||
// Received response without being intercepted in the response | ||
cache.state === 'empty') { | ||
const cache = await axios.storage.get(key); | ||
// Response already is in cache or received without | ||
// being intercepted in the response | ||
if (cache.state === 'cached' || cache.state === 'empty') { | ||
return response; | ||
} | ||
if ((_c = response.config.cache) === null || _c === void 0 ? void 0 : _c.interpretHeader) { | ||
const cacheControl = response.headers['cache-control'] || ''; | ||
const { noCache, noStore, maxAge } = (0, cache_control_1.parse)(cacheControl); | ||
const defaultMaxAge = response.config.cache?.maxAge || axios.defaults.cache.maxAge; | ||
cache.expiration = cache.expiration || defaultMaxAge; | ||
let saveCache = true; | ||
if (response.config.cache?.interpretHeader) { | ||
const expirationTime = axios.interpretHeader(response.headers['cache-control']); | ||
// Header told that this response should not be cached. | ||
if (noCache || noStore) { | ||
return response; | ||
if (expirationTime === false) { | ||
saveCache = false; | ||
} | ||
const expirationTime = maxAge | ||
? // Header max age in seconds | ||
Date.now() + maxAge * 1000 | ||
: ((_d = response.config.cache) === null || _d === void 0 ? void 0 : _d.maxAge) || axios.defaults.cache.maxAge; | ||
cache.expiration = expirationTime; | ||
else { | ||
cache.expiration = expirationTime ? expirationTime : defaultMaxAge; | ||
} | ||
} | ||
else { | ||
// If the cache expiration has not been set, use the default expiration. | ||
cache.expiration = | ||
cache.expiration || | ||
((_e = response.config.cache) === null || _e === void 0 ? void 0 : _e.maxAge) || | ||
axios.defaults.cache.maxAge; | ||
const data = { body: response.data, headers: response.headers }; | ||
const deferred = axios.waiting[key]; | ||
// Resolve all other requests waiting for this response | ||
if (deferred) { | ||
deferred.resolve(data); | ||
} | ||
const data = { body: response.data, headers: response.headers }; | ||
// Resolve this deferred to update the cache after it | ||
cache.data.resolve(data); | ||
yield axios.storage.set(key, { | ||
data, | ||
expiration: cache.expiration, | ||
state: 'cached' | ||
}); | ||
if (saveCache) { | ||
await axios.storage.set(key, { | ||
data, | ||
expiration: cache.expiration, | ||
state: 'cached' | ||
}); | ||
} | ||
return response; | ||
})); | ||
}); | ||
} | ||
exports.applyResponseInterceptor = applyResponseInterceptor; | ||
//# sourceMappingURL=response.js.map |
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.MemoryStorage = void 0; | ||
class MemoryStorage { | ||
constructor() { | ||
this.storage = new Map(); | ||
this.get = (key) => __awaiter(this, void 0, void 0, function* () { | ||
const value = this.storage.get(key); | ||
if (value) { | ||
return value; | ||
} | ||
// Fresh copy to prevent code duplication | ||
const empty = { data: null, expiration: -1, state: 'empty' }; | ||
this.storage.set(key, empty); | ||
return empty; | ||
}); | ||
this.set = (key, value) => __awaiter(this, void 0, void 0, function* () { | ||
this.storage.set(key, value); | ||
}); | ||
this.remove = (key) => __awaiter(this, void 0, void 0, function* () { | ||
this.storage.delete(key); | ||
}); | ||
this.size = () => __awaiter(this, void 0, void 0, function* () { | ||
return this.storage.size; | ||
}); | ||
this.clear = () => __awaiter(this, void 0, void 0, function* () { | ||
this.storage.clear(); | ||
}); | ||
} | ||
storage = new Map(); | ||
get = async (key) => { | ||
const value = this.storage.get(key); | ||
if (value) { | ||
return value; | ||
} | ||
const empty = { data: null, expiration: -1, state: 'empty' }; | ||
this.storage.set(key, empty); | ||
return empty; | ||
}; | ||
set = async (key, value) => { | ||
this.storage.set(key, value); | ||
}; | ||
remove = async (key) => { | ||
this.storage.delete(key); | ||
}; | ||
size = async () => { | ||
return this.storage.size; | ||
}; | ||
clear = async () => { | ||
this.storage.clear(); | ||
}; | ||
} | ||
exports.MemoryStorage = MemoryStorage; | ||
//# sourceMappingURL=memory.js.map |
@@ -1,2 +0,1 @@ | ||
import { Deferred } from '../utils/deferred'; | ||
export interface CacheStorage { | ||
@@ -29,3 +28,3 @@ /** | ||
} | { | ||
data: Deferred<CachedResponse>; | ||
data: null; | ||
/** | ||
@@ -32,0 +31,0 @@ * If interpretHeader is used, this value will be `-1`until the response is received |
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -17,19 +8,19 @@ exports.SessionCacheStorage = exports.LocalCacheStorage = exports.WindowStorageWrapper = void 0; | ||
class WindowStorageWrapper { | ||
storage; | ||
prefix; | ||
constructor(storage, prefix = 'axios-cache:') { | ||
this.storage = storage; | ||
this.prefix = prefix; | ||
this.get = (key) => __awaiter(this, void 0, void 0, function* () { | ||
const json = this.storage.getItem(this.prefix + key); | ||
return json | ||
? JSON.parse(json) | ||
: { data: null, expiration: -1, state: 'empty' }; | ||
}); | ||
this.set = (key, value) => __awaiter(this, void 0, void 0, function* () { | ||
const json = JSON.stringify(value); | ||
this.storage.setItem(this.prefix + key, json); | ||
}); | ||
this.remove = (key) => __awaiter(this, void 0, void 0, function* () { | ||
this.storage.removeItem(this.prefix + key); | ||
}); | ||
} | ||
get = async (key) => { | ||
const json = this.storage.getItem(this.prefix + key); | ||
return json ? JSON.parse(json) : { data: null, expiration: -1, state: 'empty' }; | ||
}; | ||
set = async (key, value) => { | ||
const json = JSON.stringify(value); | ||
this.storage.setItem(this.prefix + key, json); | ||
}; | ||
remove = async (key) => { | ||
this.storage.removeItem(this.prefix + key); | ||
}; | ||
} | ||
@@ -36,0 +27,0 @@ exports.WindowStorageWrapper = WindowStorageWrapper; |
{ | ||
"name": "axios-cache-interceptor", | ||
"version": "0.0.3", | ||
"version": "0.0.5", | ||
"description": "Cache interceptor for axios", | ||
@@ -25,3 +25,7 @@ "main": "dist/index.js", | ||
], | ||
"author": "Hazork", | ||
"author": { | ||
"name": "Arthur Fiorette", | ||
"email": "arthur.fiorette@gmail.com", | ||
"url": "https://arthurfiorette.com.br" | ||
}, | ||
"license": "MIT", | ||
@@ -32,3 +36,7 @@ "bugs": { | ||
"homepage": "https://github.com/ArthurFiorette/axios-cache-interceptor#readme", | ||
"dependencies": { | ||
"@tusbar/cache-control": "^0.6.0" | ||
}, | ||
"devDependencies": { | ||
"@arthurfiorette/prettier-config": "^1.0.5", | ||
"@types/node": "^16.7.10", | ||
@@ -44,7 +52,4 @@ "@typescript-eslint/eslint-plugin": "^4.30.0", | ||
"prettier-plugin-organize-imports": "^2.3.3", | ||
"typescript": "^4.4.2" | ||
}, | ||
"dependencies": { | ||
"@tusbar/cache-control": "^0.6.0" | ||
"typescript": "^4.4.3" | ||
} | ||
} |
@@ -39,2 +39,9 @@ <br /> | ||
></code> | ||
<code | ||
><a href="https://www.npmjs.com/package/axios-cache-interceptor" | ||
><img | ||
src="https://img.shields.io/npm/v/axios-cache-interceptor?color=CB3837&logo=npm&label=Npm" | ||
target="_blank" | ||
alt="Npm" /></a | ||
></code> | ||
</div> | ||
@@ -81,8 +88,10 @@ | ||
> Axios is a peer dependency and must be installed separately. | ||
```sh | ||
# Npm | ||
npm install --save axios-cache-interceptor | ||
npm install --save axios axios-cache-interceptor | ||
# Yarn | ||
yarn add axios-cache-interceptor | ||
yarn add axios axios-cache-interceptor | ||
``` | ||
@@ -94,5 +103,6 @@ | ||
This project is highly inspired by several projects, written entirely in typescript, supporting https headers and much more, | ||
This project is highly inspired by several projects, written entirely in typescript, supporting | ||
https headers and much more. | ||
Take a look at some projects: | ||
Take a look at some similar projects: | ||
@@ -113,4 +123,5 @@ - [axios-cache-adapter](https://github.com/RasCarlito/axios-cache-adapter) | ||
See my contact information on my [github profile](https://github.com/ArthurFiorette) or open a new issue. | ||
See my contact information on my [github profile](https://github.com/ArthurFiorette) or open a new | ||
issue. | ||
<br /> |
import { AxiosInstance } from 'axios'; | ||
import { defaultHeaderInterpreter } from '../header'; | ||
import { applyRequestInterceptor } from '../interceptors/request'; | ||
import { applyResponseInterceptor } from '../interceptors/response'; | ||
import { MemoryStorage } from '../storage/memory'; | ||
import { defaultKeyGenerator } from '../utils/key-generator'; | ||
import { AxiosCacheInstance, CacheInstance, CacheRequestConfig } from './types'; | ||
import { defaultKeyGenerator } from '../util/key-generator'; | ||
import CacheInstance, { AxiosCacheInstance, CacheProperties } from './types'; | ||
type Options = CacheRequestConfig['cache'] & Partial<CacheInstance>; | ||
export function createCache( | ||
axios: AxiosInstance, | ||
options: Options = {} | ||
options: Partial<CacheInstance & CacheProperties> = {} | ||
): AxiosCacheInstance { | ||
@@ -17,3 +16,5 @@ const axiosCache = axios as AxiosCacheInstance; | ||
axiosCache.storage = options.storage || new MemoryStorage(); | ||
axiosCache.generateKey = defaultKeyGenerator; | ||
axiosCache.generateKey = options.generateKey || defaultKeyGenerator; | ||
axiosCache.waiting = options.waiting || {}; | ||
axiosCache.interpretHeader = options.interpretHeader || defaultHeaderInterpreter; | ||
@@ -27,3 +28,3 @@ // CacheRequestConfig values | ||
methods: ['get'], | ||
shouldCache: ({ status }) => status >= 200 && status < 300, | ||
cachePredicate: ({ status }) => status >= 200 && status < 300, | ||
update: {}, | ||
@@ -30,0 +31,0 @@ ...options |
@@ -9,4 +9,59 @@ import type { | ||
} from 'axios'; | ||
import { CacheStorage } from '../storage/types'; | ||
import { Deferred } from 'src/util/deferred'; | ||
import { KeyGenerator } from 'src/util/key-generator'; | ||
import { HeaderInterpreter } from '../header'; | ||
import { CachedResponse, CacheStorage } from '../storage/types'; | ||
import { CachePredicate } from '../util/cache-predicate'; | ||
export type DefaultCacheRequestConfig = AxiosRequestConfig & { | ||
cache: CacheProperties; | ||
}; | ||
export type CacheProperties = { | ||
/** | ||
* The time until the cached value is expired in milliseconds. | ||
* | ||
* @default 1000 * 60 * 5 | ||
*/ | ||
maxAge: number; | ||
/** | ||
* If this interceptor should configure the cache from the request cache header | ||
* When used, the maxAge property is ignored | ||
* | ||
* @default false | ||
*/ | ||
interpretHeader: boolean; | ||
/** | ||
* All methods that should be cached. | ||
* | ||
* @default ['get'] | ||
*/ | ||
methods: Lowercase<Method>[]; | ||
/** | ||
* The function to check if the response code permit being cached. | ||
* | ||
* @default ({ status }) => status >= 200 && status < 300 | ||
*/ | ||
cachePredicate: CachePredicate; | ||
/** | ||
* Once the request is resolved, this specifies what requests should we change the cache. | ||
* Can be used to update the request or delete other caches. | ||
* | ||
* If the function returns void, the entry is deleted | ||
* | ||
* This is independent if the request made was cached or not. | ||
* | ||
* The id used is the same as the id on `CacheRequestConfig['id']`, auto-generated or not. | ||
* | ||
* @default {} | ||
*/ | ||
update: { | ||
[id: string]: 'delete' | ((oldValue: any, atual: any) => any | undefined); | ||
}; | ||
}; | ||
/** | ||
@@ -27,51 +82,6 @@ * Options that can be overridden per request | ||
*/ | ||
cache?: { | ||
/** | ||
* The time until the cached value is expired in milliseconds. | ||
* | ||
* @default 1000 * 60 * 5 | ||
*/ | ||
maxAge?: number; | ||
/** | ||
* If this interceptor should configure the cache from the request cache header | ||
* When used, the maxAge property is ignored | ||
* | ||
* @default false | ||
*/ | ||
interpretHeader?: boolean; | ||
/** | ||
* All methods that should be cached. | ||
* | ||
* @default ['get'] | ||
*/ | ||
methods?: Lowercase<Method>[]; | ||
/** | ||
* The function to check if the response code permit being cached. | ||
* | ||
* @default ({ status }) => status >= 200 && status < 300 | ||
*/ | ||
shouldCache?: (response: AxiosResponse) => boolean; | ||
/** | ||
* Once the request is resolved, this specifies what requests should we change the cache. | ||
* Can be used to update the request or delete other caches. | ||
* | ||
* If the function returns void, the entry is deleted | ||
* | ||
* This is independent if the request made was cached or not. | ||
* | ||
* The id used is the same as the id on `CacheRequestConfig['id']`, auto-generated or not. | ||
* | ||
* @default {} | ||
*/ | ||
update?: { | ||
[id: string]: 'delete' | ((oldValue: any, atual: any) => any | void); | ||
}; | ||
}; | ||
cache?: Partial<CacheProperties>; | ||
}; | ||
export interface CacheInstance { | ||
export default interface CacheInstance { | ||
/** | ||
@@ -89,5 +99,23 @@ * The storage to save the cache data. | ||
*/ | ||
generateKey: (options: CacheRequestConfig) => string; | ||
generateKey: KeyGenerator; | ||
/** | ||
* A simple object that holds all deferred objects until it is resolved. | ||
*/ | ||
waiting: Record<string, Deferred<CachedResponse>>; | ||
/** | ||
* The function to parse and interpret response headers. | ||
* Only used if cache.interpretHeader is true. | ||
*/ | ||
interpretHeader: HeaderInterpreter; | ||
} | ||
/** | ||
* Same as the AxiosInstance but with CacheRequestConfig as a config type. | ||
* | ||
* @see AxiosInstance | ||
* @see CacheRequestConfig | ||
* @see CacheInstance | ||
*/ | ||
export interface AxiosCacheInstance extends AxiosInstance, CacheInstance { | ||
@@ -97,8 +125,7 @@ (config: CacheRequestConfig): AxiosPromise; | ||
defaults: CacheRequestConfig; | ||
defaults: DefaultCacheRequestConfig; | ||
interceptors: { | ||
request: AxiosInterceptorManager<CacheRequestConfig>; | ||
response: AxiosInterceptorManager< | ||
AxiosResponse & { config: CacheRequestConfig } | ||
>; | ||
response: AxiosInterceptorManager<AxiosResponse & { config: CacheRequestConfig }>; | ||
}; | ||
@@ -108,22 +135,8 @@ | ||
request<T = any, R = AxiosResponse<T>>( | ||
config: CacheRequestConfig | ||
): Promise<R>; | ||
request<T = any, R = AxiosResponse<T>>(config: CacheRequestConfig): Promise<R>; | ||
get<T = any, R = AxiosResponse<T>>( | ||
url: string, | ||
config?: CacheRequestConfig | ||
): Promise<R>; | ||
delete<T = any, R = AxiosResponse<T>>( | ||
url: string, | ||
config?: CacheRequestConfig | ||
): Promise<R>; | ||
head<T = any, R = AxiosResponse<T>>( | ||
url: string, | ||
config?: CacheRequestConfig | ||
): Promise<R>; | ||
options<T = any, R = AxiosResponse<T>>( | ||
url: string, | ||
config?: CacheRequestConfig | ||
): Promise<R>; | ||
get<T = any, R = AxiosResponse<T>>(url: string, config?: CacheRequestConfig): Promise<R>; | ||
delete<T = any, R = AxiosResponse<T>>(url: string, config?: CacheRequestConfig): Promise<R>; | ||
head<T = any, R = AxiosResponse<T>>(url: string, config?: CacheRequestConfig): Promise<R>; | ||
options<T = any, R = AxiosResponse<T>>(url: string, config?: CacheRequestConfig): Promise<R>; | ||
post<T = any, R = AxiosResponse<T>>( | ||
@@ -130,0 +143,0 @@ url: string, |
@@ -1,3 +0,3 @@ | ||
export { createCache } from './axios/cache'; | ||
export * from './constants'; | ||
export * from './axios'; | ||
export * from './storage'; | ||
export * as StatusCodes from './util/status-codes'; |
@@ -0,16 +1,10 @@ | ||
import { CachedResponse } from 'src/storage/types'; | ||
import { AxiosCacheInstance } from '../axios/types'; | ||
import { | ||
CACHED_RESPONSE_STATUS, | ||
CACHED_RESPONSE_STATUS_TEXT | ||
} from '../constants'; | ||
import { Deferred } from '../utils/deferred'; | ||
import { Deferred } from '../util/deferred'; | ||
import { CACHED_RESPONSE_STATUS, CACHED_RESPONSE_STATUS_TEXT } from '../util/status-codes'; | ||
export function applyRequestInterceptor(axios: AxiosCacheInstance) { | ||
export function applyRequestInterceptor(axios: AxiosCacheInstance): void { | ||
axios.interceptors.request.use(async (config) => { | ||
// Only cache specified methods | ||
if ( | ||
config.cache?.methods?.some( | ||
(method) => (config.method || 'get').toLowerCase() == method | ||
) | ||
) { | ||
if (config.cache?.methods?.some((method) => (config.method || 'get').toLowerCase() == method)) { | ||
return config; | ||
@@ -24,10 +18,14 @@ } | ||
if (cache.state == 'empty') { | ||
// Create a deferred to resolve other requests for the same key when it's completed | ||
axios.waiting[key] = new Deferred(); | ||
await axios.storage.set(key, { | ||
state: 'loading', | ||
data: new Deferred(), | ||
data: null, | ||
// The cache header will be set after the response has been read, until that time, the expiration will be -1 | ||
expiration: config.cache?.interpretHeader | ||
? -1 | ||
: config.cache?.maxAge || axios.defaults.cache!.maxAge! | ||
: config.cache?.maxAge || axios.defaults.cache.maxAge | ||
}); | ||
return config; | ||
@@ -42,9 +40,22 @@ } | ||
const { body, headers } = await cache.data; | ||
let data = {} as CachedResponse; | ||
if (cache.state === 'loading') { | ||
const deferred = axios.waiting[key]; | ||
// If the deferred is undefined, means that the | ||
// outside has removed that key from the waiting list | ||
if (!deferred) { | ||
return config; | ||
} | ||
data = await deferred; | ||
} else { | ||
data = cache.data; | ||
} | ||
config.adapter = () => | ||
Promise.resolve({ | ||
data: body, | ||
config, | ||
headers, | ||
data: data.body, | ||
headers: data.headers, | ||
status: CACHED_RESPONSE_STATUS, | ||
@@ -51,0 +62,0 @@ statusText: CACHED_RESPONSE_STATUS_TEXT |
@@ -1,27 +0,24 @@ | ||
import { parse } from '@tusbar/cache-control'; | ||
import { AxiosCacheInstance } from '../axios/types'; | ||
import { checkPredicateObject } from '../util/cache-predicate'; | ||
import { updateCache } from '../util/update-cache'; | ||
export function applyResponseInterceptor(axios: AxiosCacheInstance) { | ||
export function applyResponseInterceptor(axios: AxiosCacheInstance): void { | ||
axios.interceptors.response.use(async (response) => { | ||
// Update other entries before updating himself | ||
for (const [cacheKey, value] of Object.entries( | ||
response.config.cache?.update || {} | ||
)) { | ||
if (value == 'delete') { | ||
await axios.storage.remove(cacheKey); | ||
continue; | ||
} | ||
const oldValue = await axios.storage.get(cacheKey); | ||
const newValue = value(oldValue, response.data); | ||
if (newValue !== undefined) { | ||
await axios.storage.set(cacheKey, newValue); | ||
} else { | ||
await axios.storage.remove(cacheKey); | ||
} | ||
if (response.config.cache?.update) { | ||
updateCache(axios, response.data, response.config.cache.update); | ||
} | ||
const cachePredicate = | ||
response.config.cache?.cachePredicate || axios.defaults.cache.cachePredicate; | ||
// Config told that this response should be cached. | ||
if (!response.config.cache?.shouldCache!(response)) { | ||
return response; | ||
if (typeof cachePredicate === 'function') { | ||
if (!cachePredicate(response)) { | ||
return response; | ||
} | ||
} else { | ||
if (!checkPredicateObject(response, cachePredicate)) { | ||
return response; | ||
} | ||
} | ||
@@ -32,44 +29,38 @@ | ||
if ( | ||
// Response already is in cache. | ||
cache.state === 'cached' || | ||
// Received response without being intercepted in the response | ||
cache.state === 'empty' | ||
) { | ||
// Response already is in cache or received without | ||
// being intercepted in the response | ||
if (cache.state === 'cached' || cache.state === 'empty') { | ||
return response; | ||
} | ||
const defaultMaxAge = response.config.cache?.maxAge || axios.defaults.cache.maxAge; | ||
cache.expiration = cache.expiration || defaultMaxAge; | ||
let saveCache = true; | ||
if (response.config.cache?.interpretHeader) { | ||
const cacheControl = response.headers['cache-control'] || ''; | ||
const { noCache, noStore, maxAge } = parse(cacheControl); | ||
const expirationTime = axios.interpretHeader(response.headers['cache-control']); | ||
// Header told that this response should not be cached. | ||
if (noCache || noStore) { | ||
return response; | ||
if (expirationTime === false) { | ||
saveCache = false; | ||
} else { | ||
cache.expiration = expirationTime ? expirationTime : defaultMaxAge; | ||
} | ||
const expirationTime = maxAge | ||
? // Header max age in seconds | ||
Date.now() + maxAge * 1000 | ||
: response.config.cache?.maxAge || axios.defaults.cache!.maxAge!; | ||
cache.expiration = expirationTime; | ||
} else { | ||
// If the cache expiration has not been set, use the default expiration. | ||
cache.expiration = | ||
cache.expiration || | ||
response.config.cache?.maxAge || | ||
axios.defaults.cache!.maxAge!; | ||
} | ||
const data = { body: response.data, headers: response.headers }; | ||
const deferred = axios.waiting[key]; | ||
// Resolve this deferred to update the cache after it | ||
cache.data.resolve(data); | ||
// Resolve all other requests waiting for this response | ||
if (deferred) { | ||
deferred.resolve(data); | ||
} | ||
await axios.storage.set(key, { | ||
data, | ||
expiration: cache.expiration, | ||
state: 'cached' | ||
}); | ||
if (saveCache) { | ||
await axios.storage.set(key, { | ||
data, | ||
expiration: cache.expiration, | ||
state: 'cached' | ||
}); | ||
} | ||
@@ -76,0 +67,0 @@ return response; |
@@ -13,3 +13,2 @@ import { CacheStorage, StorageValue } from './types'; | ||
// Fresh copy to prevent code duplication | ||
const empty = { data: null, expiration: -1, state: 'empty' } as const; | ||
@@ -16,0 +15,0 @@ this.storage.set(key, empty); |
@@ -1,3 +0,1 @@ | ||
import { Deferred } from '../utils/deferred'; | ||
export interface CacheStorage { | ||
@@ -34,3 +32,3 @@ /** | ||
| { | ||
data: Deferred<CachedResponse>; | ||
data: null; | ||
/** | ||
@@ -37,0 +35,0 @@ * If interpretHeader is used, this value will be `-1`until the response is received |
@@ -6,12 +6,7 @@ import { CacheStorage, StorageValue } from './types'; | ||
export abstract class WindowStorageWrapper implements CacheStorage { | ||
constructor( | ||
readonly storage: Storage, | ||
readonly prefix: string = 'axios-cache:' | ||
) {} | ||
constructor(readonly storage: Storage, readonly prefix: string = 'axios-cache:') {} | ||
get = async (key: string): Promise<StorageValue> => { | ||
const json = this.storage.getItem(this.prefix + key); | ||
return json | ||
? JSON.parse(json) | ||
: { data: null, expiration: -1, state: 'empty' }; | ||
return json ? JSON.parse(json) : { data: null, expiration: -1, state: 'empty' }; | ||
}; | ||
@@ -18,0 +13,0 @@ |
@@ -7,3 +7,3 @@ { | ||
"incremental": true /* Enable incremental compilation */, | ||
"target": "ES6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, | ||
"target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, | ||
"module": "CommonJS" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, | ||
@@ -10,0 +10,0 @@ // "lib": [], /* Specify library files to be included in the compilation. */ |
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
100790
99
1350
124
12