axios-cache-interceptor
Advanced tools
Comparing version 0.5.1 to 0.6.0
@@ -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
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
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
165836
99
2411
430