mirror of
https://github.com/arthurfiorette/axios-cache-interceptor.git
synced 2025-12-08 17:36:16 +00:00
feat: storage abstractions (#52)
* refactor: better support for different storages * test: updated tests * feat: export AxiosStorage * style: fix linting
This commit is contained in:
parent
76a8af7433
commit
b35ae3e574
9
src/cache/cache.ts
vendored
9
src/cache/cache.ts
vendored
@ -2,7 +2,8 @@ import type { Method } from 'axios';
|
||||
import type { Deferred } from 'typed-core/dist/promises/deferred';
|
||||
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';
|
||||
import type { CacheUpdater } from '../util/update-cache';
|
||||
import type { CacheAxiosResponse, CacheRequestConfig } from './axios';
|
||||
@ -54,7 +55,7 @@ export type CacheProperties = {
|
||||
* The id used is the same as the id on `CacheRequestConfig['id']`,
|
||||
* auto-generated or not.
|
||||
*
|
||||
* @default
|
||||
* @default {}
|
||||
*/
|
||||
update: Record<string, CacheUpdater>;
|
||||
};
|
||||
@ -63,9 +64,9 @@ export interface CacheInstance {
|
||||
/**
|
||||
* The storage to save the cache data.
|
||||
*
|
||||
* @default new MemoryStorage()
|
||||
* @default new MemoryAxiosStorage()
|
||||
*/
|
||||
storage: CacheStorage;
|
||||
storage: AxiosStorage;
|
||||
|
||||
/**
|
||||
* The function used to create different keys for each request.
|
||||
|
||||
4
src/cache/create.ts
vendored
4
src/cache/create.ts
vendored
@ -2,7 +2,7 @@ import Axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import { defaultHeaderInterpreter } from '../header/interpreter';
|
||||
import { CacheRequestInterceptor } from '../interceptors/request';
|
||||
import { CacheResponseInterceptor } from '../interceptors/response';
|
||||
import { MemoryStorage } from '../storage/memory';
|
||||
import { MemoryAxiosStorage } from '../storage/memory';
|
||||
import { defaultKeyGenerator } from '../util/key-generator';
|
||||
import type { AxiosCacheInstance } from './axios';
|
||||
import type { CacheInstance, CacheProperties } from './cache';
|
||||
@ -28,7 +28,7 @@ export function useCache(
|
||||
): AxiosCacheInstance {
|
||||
const axiosCache = axios as AxiosCacheInstance;
|
||||
|
||||
axiosCache.storage = storage || new MemoryStorage();
|
||||
axiosCache.storage = storage || new MemoryAxiosStorage({});
|
||||
axiosCache.generateKey = generateKey || defaultKeyGenerator;
|
||||
axiosCache.waiting = waiting || {};
|
||||
axiosCache.headerInterpreter = headerInterpreter || defaultHeaderInterpreter;
|
||||
|
||||
@ -3,5 +3,6 @@ export * from './cache/cache';
|
||||
export * from './cache/create';
|
||||
export * from './header/types';
|
||||
export * from './interceptors/types';
|
||||
export * from './storage/storage';
|
||||
export * from './storage/types';
|
||||
export * from './util/types';
|
||||
|
||||
44
src/storage/browser.ts
Normal file
44
src/storage/browser.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { AxiosStorage } from './storage';
|
||||
import type { EmptyStorageValue, StorageValue } from './types';
|
||||
|
||||
export class BrowserAxiosStorage extends AxiosStorage {
|
||||
public static DEFAULT_KEY_PREFIX = 'a-c-i';
|
||||
|
||||
/**
|
||||
* @param storage any browser storage, like sessionStorage or localStorage
|
||||
* @param prefix the key prefix to use on all keys.
|
||||
*/
|
||||
constructor(
|
||||
readonly storage: Storage,
|
||||
readonly prefix: string = BrowserAxiosStorage.DEFAULT_KEY_PREFIX
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get = (key: string): StorageValue => {
|
||||
const prefixedKey = `${this.prefix}:${key}`;
|
||||
|
||||
const json = this.storage.getItem(prefixedKey);
|
||||
|
||||
if (!json) {
|
||||
return { state: 'empty' };
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(json);
|
||||
|
||||
if (!AxiosStorage.isValid(parsed)) {
|
||||
this.storage.removeItem(prefixedKey);
|
||||
return { state: 'empty' };
|
||||
}
|
||||
|
||||
return parsed;
|
||||
};
|
||||
|
||||
public set = (key: string, value: Exclude<StorageValue, EmptyStorageValue>): void => {
|
||||
return this.storage.setItem(`${this.prefix}:${key}`, JSON.stringify(value));
|
||||
};
|
||||
|
||||
public remove = (key: string): void | Promise<void> => {
|
||||
return this.storage.removeItem(`${this.prefix}:${key}`);
|
||||
};
|
||||
}
|
||||
@ -1,17 +1,19 @@
|
||||
import type { CacheStorage, StorageValue } from './types';
|
||||
import { isCacheValid } from './util';
|
||||
import { AxiosStorage } from './storage';
|
||||
import type { CachedStorageValue, LoadingStorageValue, 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);
|
||||
public get = (key: string): StorageValue => {
|
||||
const value = this.storage[key];
|
||||
|
||||
if (!value) {
|
||||
return { state: 'empty' };
|
||||
}
|
||||
|
||||
if (isCacheValid(value) === false) {
|
||||
if (!AxiosStorage.isValid(value)) {
|
||||
this.remove(key);
|
||||
return { state: 'empty' };
|
||||
}
|
||||
@ -19,11 +21,11 @@ export class MemoryStorage implements CacheStorage {
|
||||
return value;
|
||||
};
|
||||
|
||||
set = async (key: string, value: StorageValue): Promise<void> => {
|
||||
this.storage.set(key, value);
|
||||
public set = (key: string, value: CachedStorageValue | LoadingStorageValue): void => {
|
||||
this.storage[key] = value;
|
||||
};
|
||||
|
||||
remove = async (key: string): Promise<void> => {
|
||||
this.storage.delete(key);
|
||||
public remove = (key: string): void => {
|
||||
delete this.storage[key];
|
||||
};
|
||||
}
|
||||
|
||||
38
src/storage/storage.ts
Normal file
38
src/storage/storage.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import type { EmptyStorageValue, StorageValue } from './types';
|
||||
|
||||
export abstract class AxiosStorage {
|
||||
/**
|
||||
* Returns the cached value for the given key. Must handle cache
|
||||
* miss and staling by returning a new `StorageValue` with `empty` state.
|
||||
*
|
||||
* @see {AxiosStorage#isValid}
|
||||
*/
|
||||
public abstract get: (key: string) => Promise<StorageValue> | StorageValue;
|
||||
|
||||
/**
|
||||
* Sets a new value for the given key
|
||||
*
|
||||
* Use CacheStorage.remove(key) to define a key to 'empty' state.
|
||||
*/
|
||||
public abstract set: (
|
||||
key: string,
|
||||
value: Exclude<StorageValue, EmptyStorageValue>
|
||||
) => Promise<void> | void;
|
||||
|
||||
/**
|
||||
* Removes the value for the given key
|
||||
*/
|
||||
public abstract remove: (key: string) => Promise<void> | void;
|
||||
|
||||
/**
|
||||
* Returns true if a storage value can still be used by checking his
|
||||
* createdAt and ttl values.
|
||||
*/
|
||||
static isValid = (value?: StorageValue): boolean | 'unknown' => {
|
||||
if (value?.state === 'cached') {
|
||||
return value.createdAt + value.ttl > Date.now();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@ -1,23 +1,3 @@
|
||||
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 = {
|
||||
data?: any;
|
||||
headers: Record<string, string>;
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import type { StorageValue } from './types';
|
||||
|
||||
/**
|
||||
* Returns true if a storage value can still be used by checking his
|
||||
* 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 of falsy otherwise
|
||||
*/
|
||||
export function isCacheValid(value: StorageValue): boolean | 'unknown' {
|
||||
if (!value || value.state !== 'cached') {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
return value.createdAt + value.ttl > Date.now();
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
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<StorageValue> => {
|
||||
const prefixedKey = this.prefixKey(key);
|
||||
const json = this.storage.getItem(prefixedKey);
|
||||
|
||||
if (!json) {
|
||||
return { state: 'empty' };
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(json);
|
||||
|
||||
if (isCacheValid(parsed) === false) {
|
||||
this.storage.removeItem(prefixedKey);
|
||||
return { state: 'empty' };
|
||||
}
|
||||
|
||||
return parsed;
|
||||
};
|
||||
|
||||
set = async (key: string, value: StorageValue): Promise<void> => {
|
||||
const json = JSON.stringify(value);
|
||||
this.storage.setItem(this.prefixKey(key), json);
|
||||
};
|
||||
|
||||
remove = async (key: string): Promise<void> => {
|
||||
this.storage.removeItem(this.prefixKey(key));
|
||||
};
|
||||
|
||||
private prefixKey = (key: string): string => `${this.prefix}:${key}`;
|
||||
}
|
||||
|
||||
export class LocalCacheStorage extends WindowStorageWrapper {
|
||||
constructor(prefix?: string) {
|
||||
super(window.localStorage, prefix);
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionCacheStorage extends WindowStorageWrapper {
|
||||
constructor(prefix?: string) {
|
||||
super(window.sessionStorage, prefix);
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,5 @@
|
||||
import type {
|
||||
CachedStorageValue,
|
||||
CacheStorage,
|
||||
EmptyStorageValue
|
||||
} from '../storage/types';
|
||||
import type { AxiosStorage } from '../storage/storage';
|
||||
import type { CachedStorageValue, EmptyStorageValue } from '../storage/types';
|
||||
|
||||
export type CacheUpdater =
|
||||
| 'delete'
|
||||
@ -12,7 +9,7 @@ export type CacheUpdater =
|
||||
) => CachedStorageValue | void);
|
||||
|
||||
export async function updateCache<T = any>(
|
||||
storage: CacheStorage,
|
||||
storage: AxiosStorage,
|
||||
data: T,
|
||||
entries: Record<string, CacheUpdater>
|
||||
): Promise<void> {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { MemoryStorage } from '../../src/storage/memory';
|
||||
import { MemoryAxiosStorage } from '../../src/storage/memory';
|
||||
import { testStorage } from './storages';
|
||||
|
||||
describe('tests common storages', () => {
|
||||
testStorage('memory', () => new MemoryStorage());
|
||||
testStorage('memory', () => new MemoryAxiosStorage());
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { CacheStorage } from '../../src/storage/types';
|
||||
import type { AxiosStorage } from '../../src/storage/storage';
|
||||
import { EMPTY_RESPONSE } from '../constants';
|
||||
|
||||
export function testStorage(name: string, Storage: () => CacheStorage): void {
|
||||
export function testStorage(name: string, Storage: () => AxiosStorage): void {
|
||||
it(`tests ${name} storage methods`, async () => {
|
||||
const storage = Storage();
|
||||
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
import { isCacheValid } from '../../src/storage/util';
|
||||
import { AxiosStorage } from '../../src/storage/storage';
|
||||
|
||||
describe('tests common storages', () => {
|
||||
it('tests isCacheValid with empty state', () => {
|
||||
const invalid = isCacheValid({ state: 'empty' });
|
||||
const invalid = AxiosStorage.isValid({ state: 'empty' });
|
||||
|
||||
expect(invalid).toBe('unknown');
|
||||
expect(invalid).toBe(true);
|
||||
});
|
||||
|
||||
it('tests isCacheValid with loading state', () => {
|
||||
const invalid = isCacheValid({ state: 'loading' });
|
||||
const invalid = AxiosStorage.isValid({ state: 'loading' });
|
||||
|
||||
expect(invalid).toBe('unknown');
|
||||
expect(invalid).toBe(true);
|
||||
});
|
||||
|
||||
it('tests isCacheValid with overdue cached state', () => {
|
||||
const isValid = isCacheValid({
|
||||
const isValid = AxiosStorage.isValid({
|
||||
state: 'cached',
|
||||
data: {} as any, // doesn't matter
|
||||
createdAt: Date.now() - 2000, // 2 seconds in the past
|
||||
@ -24,8 +24,8 @@ describe('tests common storages', () => {
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('tests isCacheValid with overdue cached state', () => {
|
||||
const isValid = isCacheValid({
|
||||
it('tests isCacheValid with cached state', () => {
|
||||
const isValid = AxiosStorage.isValid({
|
||||
state: 'cached',
|
||||
data: {} as any, // doesn't matter
|
||||
createdAt: Date.now(),
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { LocalCacheStorage, SessionCacheStorage } from '../../src/storage/web';
|
||||
import { BrowserAxiosStorage } from '../../src/storage/browser';
|
||||
import { testStorage } from './storages';
|
||||
|
||||
describe('tests web storages', () => {
|
||||
testStorage('local-storage', () => new LocalCacheStorage());
|
||||
testStorage('session-storage', () => new SessionCacheStorage());
|
||||
testStorage('local-storage', () => new BrowserAxiosStorage(localStorage));
|
||||
testStorage('session-storage', () => new BrowserAxiosStorage(sessionStorage));
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user