mirror of
https://github.com/arthurfiorette/axios-cache-interceptor.git
synced 2025-12-08 17:36:16 +00:00
feat: suupport for async CacheUpdater
This commit is contained in:
parent
ce2f5976bc
commit
cfbd601b98
@ -1,10 +1,9 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import type { CacheProperties } from '..';
|
||||
import type { CacheAxiosResponse, CacheProperties } from '..';
|
||||
import type { CachePredicateObject } from './types';
|
||||
|
||||
/** Returns true if the response should be cached */
|
||||
export function shouldCacheResponse<R>(
|
||||
response: AxiosResponse<R>,
|
||||
export function shouldCacheResponse<R, D>(
|
||||
response: CacheAxiosResponse<R, D>,
|
||||
{ cachePredicate }: CacheProperties
|
||||
) {
|
||||
if (typeof cachePredicate === 'function') {
|
||||
@ -14,9 +13,9 @@ export function shouldCacheResponse<R>(
|
||||
return isCachePredicateValid(response, cachePredicate);
|
||||
}
|
||||
|
||||
export function isCachePredicateValid<R>(
|
||||
response: AxiosResponse<R>,
|
||||
{ statusCheck, containsHeaders, responseMatch }: CachePredicateObject
|
||||
export function isCachePredicateValid<R, D>(
|
||||
response: CacheAxiosResponse<R, D>,
|
||||
{ statusCheck, containsHeaders, responseMatch }: CachePredicateObject<R, D>
|
||||
): boolean {
|
||||
if (statusCheck) {
|
||||
if (typeof statusCheck === 'function') {
|
||||
@ -60,7 +59,7 @@ export function isCachePredicateValid<R>(
|
||||
}
|
||||
}
|
||||
|
||||
if (responseMatch && !responseMatch(response.data)) {
|
||||
if (responseMatch && !responseMatch(response)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import type { CacheRequestConfig } from '../cache/axios';
|
||||
import type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios';
|
||||
import type {
|
||||
CachedStorageValue,
|
||||
LoadingStorageValue,
|
||||
StorageValue
|
||||
} from '../storage/types';
|
||||
|
||||
export type CachePredicate =
|
||||
| CachePredicateObject
|
||||
| (<R>(response: AxiosResponse<R>) => boolean);
|
||||
export type CachePredicate<R = any, D = any> =
|
||||
| CachePredicateObject<R, D>
|
||||
| (<R, D>(response: CacheAxiosResponse<R, D>) => boolean);
|
||||
|
||||
export type CachePredicateObject = {
|
||||
export type CachePredicateObject<R = any, D = any> = {
|
||||
/**
|
||||
* The status predicate, if a tuple is returned, the first and seconds value means the
|
||||
* interval (inclusive) accepted. Can also be a function.
|
||||
@ -19,8 +23,19 @@ export type CachePredicateObject = {
|
||||
containsHeaders?: Record<string, true | string | ((header: string) => boolean)>;
|
||||
|
||||
/** Check if the desired response matches this predicate. */
|
||||
responseMatch?: <T = any>(res: T | undefined) => boolean;
|
||||
responseMatch?: (res: CacheAxiosResponse<R, D>) => boolean;
|
||||
};
|
||||
|
||||
/** A simple function that receives a cache request config and should return a string id for it. */
|
||||
export type KeyGenerator = <R>(options: CacheRequestConfig<R>) => string;
|
||||
export type KeyGenerator = <R = any, D = any>(
|
||||
options: CacheRequestConfig<R, D>
|
||||
) => string;
|
||||
|
||||
type MaybePromise<T> = T | Promise<T> | PromiseLike<T>;
|
||||
|
||||
export type CacheUpdater<R, D> =
|
||||
| 'delete'
|
||||
| ((
|
||||
cached: Exclude<StorageValue, LoadingStorageValue>,
|
||||
response: CacheAxiosResponse<R, D>
|
||||
) => MaybePromise<CachedStorageValue | 'delete' | 'ignore'>);
|
||||
|
||||
@ -1,22 +1,12 @@
|
||||
import type {
|
||||
AxiosStorage,
|
||||
CachedStorageValue,
|
||||
LoadingStorageValue,
|
||||
StorageValue
|
||||
} from '../storage/types';
|
||||
|
||||
export type CacheUpdater =
|
||||
| 'delete'
|
||||
| ((
|
||||
cached: Exclude<StorageValue, LoadingStorageValue>,
|
||||
newData: any
|
||||
) => CachedStorageValue | void);
|
||||
import type { AxiosStorage } from '..';
|
||||
import type { CacheAxiosResponse } from '../cache/axios';
|
||||
import type { CacheUpdater } from './types';
|
||||
|
||||
/** Function to update all caches, from CacheProperties.update, with the new data. */
|
||||
export async function updateCache<T = any>(
|
||||
export async function updateCache<T, D>(
|
||||
storage: AxiosStorage,
|
||||
data: T,
|
||||
entries: Record<string, CacheUpdater>
|
||||
data: CacheAxiosResponse<T, D>,
|
||||
entries: Record<string, CacheUpdater<T, D>>
|
||||
): Promise<void> {
|
||||
for (const cacheKey in entries) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
@ -30,16 +20,18 @@ export async function updateCache<T = any>(
|
||||
const oldValue = await storage.get(cacheKey);
|
||||
|
||||
if (oldValue.state === 'loading') {
|
||||
throw new Error('cannot update the cache while loading');
|
||||
continue;
|
||||
}
|
||||
|
||||
const newValue = value(oldValue, data);
|
||||
const newValue = await value(oldValue, data);
|
||||
|
||||
if (newValue === undefined) {
|
||||
if (newValue === 'delete') {
|
||||
await storage.remove(cacheKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
await storage.set(cacheKey, newValue);
|
||||
if (newValue !== 'ignore') {
|
||||
await storage.set(cacheKey, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import type { CachedStorageValue } from '../../src';
|
||||
import { isCachePredicateValid } from '../../src/util/cache-predicate';
|
||||
import { mockAxios } from '../mocks/axios';
|
||||
import { createResponse } from '../utils';
|
||||
|
||||
describe('tests cache predicate object', () => {
|
||||
@ -94,14 +96,50 @@ describe('tests cache predicate object', () => {
|
||||
});
|
||||
|
||||
const testStrict = isCachePredicateValid(response, {
|
||||
responseMatch: (data: any) => data && data.a === true && data.b === 1
|
||||
responseMatch: ({ data }) => data && data.a === true && data.b === 1
|
||||
});
|
||||
|
||||
const testError = isCachePredicateValid(response, {
|
||||
responseMatch: (data: any) => data && (data.a !== true || data.b !== 1)
|
||||
responseMatch: ({ data }) => data && (data.a !== true || data.b !== 1)
|
||||
});
|
||||
|
||||
expect(testStrict).toBeTruthy();
|
||||
expect(testError).toBeFalsy();
|
||||
});
|
||||
|
||||
it('tests generics and typescript types', () => {
|
||||
() => {
|
||||
const axios = mockAxios();
|
||||
axios.get<{ a: boolean; b: number }>('/', {
|
||||
cache: {
|
||||
ttl: ({ data }) => {
|
||||
return data.b;
|
||||
},
|
||||
cachePredicate: {
|
||||
responseMatch: ({ data }) => {
|
||||
return data.a;
|
||||
}
|
||||
},
|
||||
update: {
|
||||
id: (
|
||||
_,
|
||||
{ data: { a, b }, headers, status, statusText }
|
||||
): CachedStorageValue => {
|
||||
return {
|
||||
state: 'cached',
|
||||
ttl: Infinity,
|
||||
createdAt: Date.now(),
|
||||
data: {
|
||||
headers,
|
||||
status,
|
||||
statusText,
|
||||
data: { a, b }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@ -87,4 +87,30 @@ describe('tests key generation', () => {
|
||||
|
||||
expect(keyABOrder).toBe(keyBAOrder);
|
||||
});
|
||||
|
||||
it('tests argument replacement', () => {
|
||||
const key = defaultKeyGenerator({
|
||||
baseURL: 'http://example.com',
|
||||
url: '',
|
||||
params: { a: 1, b: 2 }
|
||||
});
|
||||
|
||||
expect(key).toBe('get::http://example.com::{"a":1,"b":2}');
|
||||
|
||||
const groups = [
|
||||
['http://example.com', '/http://example.com'],
|
||||
['http://example.com', '/http://example.com/'],
|
||||
['http://example.com/', '/http://example.com'],
|
||||
['http://example.com/', '/http://example.com/']
|
||||
];
|
||||
|
||||
for (const [first, second] of groups) {
|
||||
expect(defaultKeyGenerator({ url: first })).toBe(
|
||||
defaultKeyGenerator({ url: second })
|
||||
);
|
||||
expect(defaultKeyGenerator({ baseURL: first })).toBe(
|
||||
defaultKeyGenerator({ baseURL: second })
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,88 +1,123 @@
|
||||
import type { AxiosCacheInstance, CachedStorageValue } from '../../src';
|
||||
import { updateCache } from '../../src/util/update-cache';
|
||||
import type { CachedStorageValue } from '../../src';
|
||||
import { defaultKeyGenerator } from '../../src/util/key-generator';
|
||||
import { mockAxios } from '../mocks/axios';
|
||||
import { EMPTY_RESPONSE } from '../utils';
|
||||
|
||||
const KEY = 'cacheKey';
|
||||
const EMPTY_STATE = { state: 'empty' };
|
||||
const DEFAULT_DATA = 'random-data';
|
||||
const INITIAL_DATA: CachedStorageValue = {
|
||||
data: {
|
||||
...EMPTY_RESPONSE,
|
||||
data: true
|
||||
},
|
||||
const cacheKey = defaultKeyGenerator({ url: 'https://example.com/' });
|
||||
const cachedValue: CachedStorageValue = {
|
||||
createdAt: Date.now(),
|
||||
ttl: Infinity,
|
||||
state: 'cached'
|
||||
state: 'cached',
|
||||
ttl: Infinity, // never expires
|
||||
data: {
|
||||
data: 'value',
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: '200 OK'
|
||||
}
|
||||
};
|
||||
|
||||
describe('Tests update-cache', () => {
|
||||
let axios: AxiosCacheInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
axios = mockAxios({});
|
||||
axios.storage.set(KEY, INITIAL_DATA);
|
||||
});
|
||||
|
||||
it('tests for delete key', async () => {
|
||||
await updateCache(axios.storage, DEFAULT_DATA, {
|
||||
[KEY]: 'delete'
|
||||
const axios = mockAxios({});
|
||||
await axios.storage.set(cacheKey, cachedValue);
|
||||
|
||||
await axios.get('other-key', {
|
||||
cache: { update: { [cacheKey]: 'delete' } }
|
||||
});
|
||||
|
||||
const response = await axios.storage.get(KEY);
|
||||
const cacheValue1 = await axios.storage.get(cacheKey);
|
||||
expect(cacheValue1).toStrictEqual({ state: 'empty' });
|
||||
|
||||
expect(response).not.toBeFalsy();
|
||||
expect(response).toStrictEqual(EMPTY_STATE);
|
||||
});
|
||||
//
|
||||
|
||||
it('tests for returning undefined', async () => {
|
||||
await updateCache(axios.storage, DEFAULT_DATA, {
|
||||
[KEY]: () => undefined
|
||||
});
|
||||
await axios.storage.set(cacheKey, cachedValue);
|
||||
|
||||
const response = await axios.storage.get(KEY);
|
||||
|
||||
expect(response).not.toBeFalsy();
|
||||
expect(response).toStrictEqual(EMPTY_STATE);
|
||||
});
|
||||
|
||||
it('tests for returning an new value', async () => {
|
||||
await updateCache(axios.storage, DEFAULT_DATA, {
|
||||
[KEY]: (cached, newData) => ({
|
||||
state: 'cached',
|
||||
ttl: Infinity,
|
||||
createdAt: Date.now(),
|
||||
data: {
|
||||
...EMPTY_RESPONSE,
|
||||
data: `${cached.data?.data}:${newData}`
|
||||
await axios.get('other-key2', {
|
||||
cache: {
|
||||
update: {
|
||||
[cacheKey]: () => 'delete'
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const response = await axios.storage.get(KEY);
|
||||
const cacheValue2 = await axios.storage.get(cacheKey);
|
||||
expect(cacheValue2).toStrictEqual({ state: 'empty' });
|
||||
|
||||
expect(response).not.toBeFalsy();
|
||||
expect(response).not.toStrictEqual(EMPTY_STATE);
|
||||
//
|
||||
|
||||
expect(response.state).toBe('cached');
|
||||
expect(response.data?.data).toBe(`${INITIAL_DATA.data?.data}:${DEFAULT_DATA}`);
|
||||
await axios.storage.set(cacheKey, cachedValue);
|
||||
|
||||
await axios.get('other-key3', {
|
||||
cache: { update: { [cacheKey]: () => Promise.resolve('delete') } }
|
||||
});
|
||||
|
||||
const cacheValue3 = await axios.storage.get(cacheKey);
|
||||
expect(cacheValue3).toStrictEqual({ state: 'empty' });
|
||||
});
|
||||
|
||||
it('check if the state is loading while updating', async () => {
|
||||
axios.storage.set(KEY, { state: 'loading' });
|
||||
it('tests for ignore key', async () => {
|
||||
const axios = mockAxios({});
|
||||
await axios.storage.set(cacheKey, cachedValue);
|
||||
|
||||
const result = updateCache(axios.storage, DEFAULT_DATA, {
|
||||
[KEY]: (cached, newData) => ({
|
||||
state: 'cached',
|
||||
ttl: Infinity,
|
||||
createdAt: Date.now(),
|
||||
data: {
|
||||
...EMPTY_RESPONSE,
|
||||
data: `${cached.data?.data}:${newData}`
|
||||
}
|
||||
})
|
||||
await axios.get('other-key', {
|
||||
cache: { update: { [cacheKey]: () => 'ignore' } }
|
||||
});
|
||||
|
||||
expect(result).rejects.toThrowError();
|
||||
const cacheValue = await axios.storage.get(cacheKey);
|
||||
expect(cacheValue).toStrictEqual(cachedValue);
|
||||
|
||||
//
|
||||
|
||||
await axios.get('other-key2', {
|
||||
cache: { update: { [cacheKey]: async () => Promise.resolve('ignore') } }
|
||||
});
|
||||
|
||||
const cacheValue2 = await axios.storage.get(cacheKey);
|
||||
expect(cacheValue2).toStrictEqual(cachedValue);
|
||||
});
|
||||
|
||||
it('tests for new cached storage value', async () => {
|
||||
const axios = mockAxios({});
|
||||
await axios.storage.set(cacheKey, cachedValue);
|
||||
|
||||
await axios.get('other-key', {
|
||||
cache: {
|
||||
update: {
|
||||
[cacheKey]: (cached) => {
|
||||
if (cached.state !== 'cached') {
|
||||
return 'ignore';
|
||||
}
|
||||
|
||||
return {
|
||||
...cached,
|
||||
data: { ...cached.data, data: 1 }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const cacheValue = await axios.storage.get(cacheKey);
|
||||
expect(cacheValue).not.toStrictEqual(cachedValue);
|
||||
expect(cacheValue.data?.data).toBe(1);
|
||||
});
|
||||
|
||||
it('tests updateCache with key is loading', async () => {
|
||||
const axios = mockAxios({});
|
||||
await axios.storage.set(cacheKey, { state: 'loading' });
|
||||
|
||||
const handler = jest.fn();
|
||||
|
||||
await axios.get('other-key', {
|
||||
cache: {
|
||||
update: {
|
||||
[cacheKey]: handler
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
|
||||
const cacheValue = await axios.storage.get(cacheKey);
|
||||
expect(cacheValue.state).toBe('loading');
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import type { CacheAxiosResponse } from '../src/cache/axios';
|
||||
|
||||
export const EMPTY_RESPONSE = {
|
||||
headers: {},
|
||||
@ -7,10 +7,16 @@ export const EMPTY_RESPONSE = {
|
||||
data: true
|
||||
};
|
||||
|
||||
export const createResponse = <R>(
|
||||
config: Partial<AxiosResponse<R>>
|
||||
): AxiosResponse<R> => {
|
||||
return { ...EMPTY_RESPONSE, config: {}, data: {} as R, request: {}, ...config };
|
||||
export const createResponse = <R>(config: Partial<CacheAxiosResponse<R>>) => {
|
||||
return {
|
||||
...EMPTY_RESPONSE,
|
||||
config: {},
|
||||
data: {} as R,
|
||||
request: {},
|
||||
id: 'empty-id',
|
||||
cached: true,
|
||||
...config
|
||||
};
|
||||
};
|
||||
|
||||
export const sleep = (ms: number): Promise<void> =>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user