diff --git a/src/storage/memory.ts b/src/storage/memory.ts index d907e28..4a3713e 100644 --- a/src/storage/memory.ts +++ b/src/storage/memory.ts @@ -11,7 +11,7 @@ export class MemoryStorage implements CacheStorage { return { state: 'empty' }; } - if (!isCacheValid(value)) { + if (isCacheValid(value) === false) { this.remove(key); return { state: 'empty' }; } diff --git a/src/storage/types.ts b/src/storage/types.ts index f581dc3..a712025 100644 --- a/src/storage/types.ts +++ b/src/storage/types.ts @@ -32,6 +32,10 @@ export type StorageValue = CachedStorageValue | LoadingStorageValue | EmptyStora export type CachedStorageValue = { data: CachedResponse; + /** + * The number in milliseconds to wait after createdAt before the + * value is considered stale. + */ ttl: number; createdAt: number; state: 'cached'; diff --git a/src/storage/util.ts b/src/storage/util.ts index 35f38af..af293e8 100644 --- a/src/storage/util.ts +++ b/src/storage/util.ts @@ -2,18 +2,15 @@ import type { StorageValue } from './types'; /** * Returns true if a storage value can still be used by checking his - * createdAt and ttl values. + * createdAt and ttl values. Returns `'unknown'` when the cache.state + * is different from `'cached'` * * @param value The stored value - * @returns True if the cache can still be used + * @returns True if the cache can still be used of falsy otherwise */ -export function isCacheValid(value: StorageValue): boolean { - if (!value) { - return false; - } - - if (value.state !== 'cached') { - return false; +export function isCacheValid(value: StorageValue): boolean | 'unknown' { + if (!value || value.state !== 'cached') { + return 'unknown'; } return value.createdAt + value.ttl > Date.now(); diff --git a/src/storage/web.ts b/src/storage/web.ts index b8bb8b8..6208777 100644 --- a/src/storage/web.ts +++ b/src/storage/web.ts @@ -1,10 +1,26 @@ import type { CacheStorage, StorageValue } from './types'; import { isCacheValid } from './util'; +/** + * The key prefix used in WindowStorageWrapper to prevent key + * collisions with other code. + */ +export const DEFAULT_KEY_PREFIX = 'axios-cache-interceptor'; + /** * A storage that uses any {@link Storage} as his storage. + * + * **Note**: All storage keys used are prefixed with `prefix` value. */ export abstract class WindowStorageWrapper implements CacheStorage { + /** + * Creates a new instance of WindowStorageWrapper + * + * @param storage The storage to interact + * @param prefix The prefix to use for all keys or + * `DEFAULT_KEY_PREFIX` if not provided. + * @see DEFAULT_KEY_PREFIX + */ constructor(readonly storage: Storage, readonly prefix: string = DEFAULT_KEY_PREFIX) {} get = async (key: string): Promise => { @@ -17,7 +33,7 @@ export abstract class WindowStorageWrapper implements CacheStorage { const parsed = JSON.parse(json); - if (!isCacheValid(parsed)) { + if (isCacheValid(parsed) === false) { this.storage.removeItem(prefixedKey); return { state: 'empty' }; } @@ -48,5 +64,3 @@ export class SessionCacheStorage extends WindowStorageWrapper { super(window.sessionStorage, prefix); } } - -export const DEFAULT_KEY_PREFIX = 'axios-cache-interceptor'; diff --git a/test/storage/util.test.ts b/test/storage/util.test.ts new file mode 100644 index 0000000..15fddab --- /dev/null +++ b/test/storage/util.test.ts @@ -0,0 +1,37 @@ +import { isCacheValid } from '../../src/storage/util'; + +describe('tests common storages', () => { + it('tests isCacheValid with empty state', () => { + const invalid = isCacheValid({ state: 'empty' }); + + expect(invalid).toBe('unknown'); + }); + + it('tests isCacheValid with loading state', () => { + const invalid = isCacheValid({ state: 'loading' }); + + expect(invalid).toBe('unknown'); + }); + + it('tests isCacheValid with overdue cached state', () => { + const isValid = isCacheValid({ + state: 'cached', + data: {} as any, // doesn't matter + createdAt: Date.now() - 2000, // 2 seconds in the past + ttl: 1000 // 1 second + }); + + expect(isValid).toBe(false); + }); + + it('tests isCacheValid with overdue cached state', () => { + const isValid = isCacheValid({ + state: 'cached', + data: {} as any, // doesn't matter + createdAt: Date.now(), + ttl: 1000 // 1 second + }); + + expect(isValid).toBe(true); + }); +});