diff --git a/src/util/cache-predicate.ts b/src/util/cache-predicate.ts index 05dc209..25154b2 100644 --- a/src/util/cache-predicate.ts +++ b/src/util/cache-predicate.ts @@ -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( - response: AxiosResponse, +export function shouldCacheResponse( + response: CacheAxiosResponse, { cachePredicate }: CacheProperties ) { if (typeof cachePredicate === 'function') { @@ -14,9 +13,9 @@ export function shouldCacheResponse( return isCachePredicateValid(response, cachePredicate); } -export function isCachePredicateValid( - response: AxiosResponse, - { statusCheck, containsHeaders, responseMatch }: CachePredicateObject +export function isCachePredicateValid( + response: CacheAxiosResponse, + { statusCheck, containsHeaders, responseMatch }: CachePredicateObject ): boolean { if (statusCheck) { if (typeof statusCheck === 'function') { @@ -60,7 +59,7 @@ export function isCachePredicateValid( } } - if (responseMatch && !responseMatch(response.data)) { + if (responseMatch && !responseMatch(response)) { return false; } diff --git a/src/util/types.ts b/src/util/types.ts index 51ffef6..2cb97f1 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -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 - | ((response: AxiosResponse) => boolean); +export type CachePredicate = + | CachePredicateObject + | ((response: CacheAxiosResponse) => boolean); -export type CachePredicateObject = { +export type CachePredicateObject = { /** * 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 boolean)>; /** Check if the desired response matches this predicate. */ - responseMatch?: (res: T | undefined) => boolean; + responseMatch?: (res: CacheAxiosResponse) => boolean; }; /** A simple function that receives a cache request config and should return a string id for it. */ -export type KeyGenerator = (options: CacheRequestConfig) => string; +export type KeyGenerator = ( + options: CacheRequestConfig +) => string; + +type MaybePromise = T | Promise | PromiseLike; + +export type CacheUpdater = + | 'delete' + | (( + cached: Exclude, + response: CacheAxiosResponse + ) => MaybePromise); diff --git a/src/util/update-cache.ts b/src/util/update-cache.ts index db40cfd..f6fed44 100644 --- a/src/util/update-cache.ts +++ b/src/util/update-cache.ts @@ -1,22 +1,12 @@ -import type { - AxiosStorage, - CachedStorageValue, - LoadingStorageValue, - StorageValue -} from '../storage/types'; - -export type CacheUpdater = - | 'delete' - | (( - cached: Exclude, - 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( +export async function updateCache( storage: AxiosStorage, - data: T, - entries: Record + data: CacheAxiosResponse, + entries: Record> ): Promise { for (const cacheKey in entries) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -30,16 +20,18 @@ export async function updateCache( 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); + } } } diff --git a/test/util/cache-predicate.test.ts b/test/util/cache-predicate.test.ts index 1b807e3..d9cb2e7 100644 --- a/test/util/cache-predicate.test.ts +++ b/test/util/cache-predicate.test.ts @@ -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 } + } + }; + } + } + } + }); + }; + }); }); diff --git a/test/util/key-generator.test.ts b/test/util/key-generator.test.ts index c63c6fa..14cfad8 100644 --- a/test/util/key-generator.test.ts +++ b/test/util/key-generator.test.ts @@ -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 }) + ); + } + }); }); diff --git a/test/util/update-cache.test.ts b/test/util/update-cache.test.ts index 6edfc73..a4d22d1 100644 --- a/test/util/update-cache.test.ts +++ b/test/util/update-cache.test.ts @@ -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'); }); }); diff --git a/test/utils.ts b/test/utils.ts index b9274bd..e965af8 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -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 = ( - config: Partial> -): AxiosResponse => { - return { ...EMPTY_RESPONSE, config: {}, data: {} as R, request: {}, ...config }; +export const createResponse = (config: Partial>) => { + return { + ...EMPTY_RESPONSE, + config: {}, + data: {} as R, + request: {}, + id: 'empty-id', + cached: true, + ...config + }; }; export const sleep = (ms: number): Promise =>