diff --git a/README.md b/README.md index ed8965c..ec8b297 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,24 @@ Axios Cache Interceptor
-```sh -# TODO: Example +```ts +import axios from 'axios'; +import { createCache, SessionCacheStorage } from 'axios-cache-interceptor'; + +// Any custom axios instance +const api = axios.create(); + +// Other axios instance with caching enabled +const cache = createCache(api, { + // Store values on window.sessionStorage + storage: new SessionCacheStorage(), + + // Use the max-age header to determina the cache expiration time + interpretHeader: true +}); + +// Exactly the same as before +cache.get('http://example.com/'); ```
diff --git a/docs/readme.ts b/docs/readme.ts new file mode 100644 index 0000000..c318325 --- /dev/null +++ b/docs/readme.ts @@ -0,0 +1,19 @@ +// Readme example + +import axios from 'axios'; +import { createCache, SessionCacheStorage } from '../src/index'; + +// Any custom axios instance +const api = axios.create(); + +// Other axios instance with caching enabled +const cache = createCache(api, { + // Store values on window.sessionStorage + storage: new SessionCacheStorage(), + + // Use the max-age header to determina the cache expiration time + interpretHeader: true +}); + +// Exactly the same as before +cache.get('http://example.com/'); diff --git a/src/axios/cache.ts b/src/axios/cache.ts new file mode 100644 index 0000000..18bf000 --- /dev/null +++ b/src/axios/cache.ts @@ -0,0 +1,33 @@ +import { AxiosInstance } from 'axios'; +import { applyRequestInterceptor } from '../interceptors/request'; +import { applyResponseInterceptor } from '../interceptors/response'; +import { MemoryStorage } from '../storage/memory'; +import { AxiosCacheInstance, CacheInstance, CacheRequestConfig } from './types'; + +type Options = CacheRequestConfig['cache'] & Partial; + +export function createCache( + axios: AxiosInstance, + options: Options = {} +): AxiosCacheInstance { + const axiosCache = axios as AxiosCacheInstance; + + axiosCache.storage = options.storage || new MemoryStorage(); + + // CacheRequestConfig values + axiosCache.defaults = { + ...axios.defaults, + cache: { + maxAge: 1000 * 60 * 5, + interpretHeader: false, + methods: ['get'], + ...options + } + }; + + // Apply interceptors + applyRequestInterceptor(axiosCache); + applyResponseInterceptor(axiosCache); + + return axiosCache; +} diff --git a/src/axios/types.ts b/src/axios/types.ts new file mode 100644 index 0000000..fd6c69f --- /dev/null +++ b/src/axios/types.ts @@ -0,0 +1,101 @@ +import type { + AxiosInstance, + AxiosInterceptorManager, + AxiosPromise, + AxiosRequestConfig, + AxiosResponse, + Method +} from 'axios'; +import { CacheStorage } from '../storage/types'; + +/** + * Options that can be overridden per request + */ +export type CacheRequestConfig = AxiosRequestConfig & { + /** + * All cache options for the request + */ + cache?: { + /** + * The time until the cached value is expired in milliseconds. + * + * @default 1000 * 60 * 5 + */ + maxAge?: number; + + /** + * If this interceptor should configure the cache from the request cache header + * When used, the maxAge property is ignored + * + * @default false + */ + interpretHeader?: boolean; + + /** + * All methods that should be cached. + * + * @default ['get'] + */ + methods?: Lowercase[]; + }; +}; + +export interface CacheInstance { + /** + * The storage to save the cache data. + * + * @default new MemoryStorage() + */ + storage: CacheStorage; +} + +export interface AxiosCacheInstance extends AxiosInstance, CacheInstance { + (config: CacheRequestConfig): AxiosPromise; + (url: string, config?: CacheRequestConfig): AxiosPromise; + + defaults: CacheRequestConfig; + interceptors: { + request: AxiosInterceptorManager; + response: AxiosInterceptorManager< + AxiosResponse & { config: CacheRequestConfig } + >; + }; + + getUri(config?: CacheRequestConfig): string; + + request>( + config: CacheRequestConfig + ): Promise; + + get>( + url: string, + config?: CacheRequestConfig + ): Promise; + delete>( + url: string, + config?: CacheRequestConfig + ): Promise; + head>( + url: string, + config?: CacheRequestConfig + ): Promise; + options>( + url: string, + config?: CacheRequestConfig + ): Promise; + post>( + url: string, + data?: any, + config?: CacheRequestConfig + ): Promise; + put>( + url: string, + data?: any, + config?: CacheRequestConfig + ): Promise; + patch>( + url: string, + data?: any, + config?: CacheRequestConfig + ): Promise; +} diff --git a/src/index.ts b/src/index.ts index 7372ed0..29311b1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ -export function sum(a: number, b: number): number { - return a + b; -} +export { createCache } from './axios/cache'; +export * from './storage'; diff --git a/src/interceptors/request.ts b/src/interceptors/request.ts new file mode 100644 index 0000000..5fa56ec --- /dev/null +++ b/src/interceptors/request.ts @@ -0,0 +1,7 @@ +import { AxiosCacheInstance } from '../axios/types'; + +export function applyRequestInterceptor(axios: AxiosCacheInstance) { + axios.interceptors.request.use(async (config) => { + return config; + }); +} diff --git a/src/interceptors/response.ts b/src/interceptors/response.ts new file mode 100644 index 0000000..8817ac1 --- /dev/null +++ b/src/interceptors/response.ts @@ -0,0 +1,7 @@ +import { AxiosCacheInstance } from '../axios/types'; + +export function applyResponseInterceptor(axios: AxiosCacheInstance) { + axios.interceptors.response.use(async (config) => { + return config; + }); +} diff --git a/src/storage/index.ts b/src/storage/index.ts new file mode 100644 index 0000000..5dd52b5 --- /dev/null +++ b/src/storage/index.ts @@ -0,0 +1,4 @@ +export * from './memory'; +export * from './types'; +export * from './web'; +export * from './wrapper'; diff --git a/src/storage/memory.ts b/src/storage/memory.ts new file mode 100644 index 0000000..a5996c9 --- /dev/null +++ b/src/storage/memory.ts @@ -0,0 +1,41 @@ +import { CacheStorage, StorageValue } from './types'; + +const emptyValue: StorageValue = { + data: null, + expires: -1, + state: 'empty' +}; + +export class MemoryStorage implements CacheStorage { + readonly storage: Map = new Map(); + + get = async (key: string): Promise => { + const value = this.storage.get(key); + + if (value) { + return value; + } + + // Fresh copy to prevent code duplication + const empty = { ...emptyValue }; + + this.storage.set(key, empty); + return empty; + }; + + set = async (key: string, value: StorageValue): Promise => { + this.storage.set(key, value); + }; + + remove = async (key: string): Promise => { + this.storage.delete(key); + }; + + size = async (): Promise => { + return this.storage.size; + }; + + clear = async (): Promise => { + this.storage.clear(); + }; +} diff --git a/src/storage/types.ts b/src/storage/types.ts new file mode 100644 index 0000000..5ecd414 --- /dev/null +++ b/src/storage/types.ts @@ -0,0 +1,32 @@ +export interface CacheStorage { + /** + * Returns the cached value for the given key or a new empty + */ + get: (key: string) => Promise; + /** + * Sets a new value for the given key + */ + set: (key: string, value: StorageValue) => Promise; + /** + * Removes the value for the given key + */ + remove: (key: string) => Promise; +} + +export interface StorageValue { + /** + * The value of the cached response + */ + data: any | null; + + /** + * The time when the cached response expires + * -1 means not cached + */ + expires: number; + + /** + * The status of this value. + */ + state: 'cached' | 'empty' | 'loading'; +} diff --git a/src/storage/web.ts b/src/storage/web.ts new file mode 100644 index 0000000..0f3a30c --- /dev/null +++ b/src/storage/web.ts @@ -0,0 +1,13 @@ +import { WindowStorageWrapper } from './wrapper'; + +export class LocalCacheStorage extends WindowStorageWrapper { + constructor(prefix?: string) { + super(window.localStorage, prefix); + } +} + +export class SessionCacheStorage extends WindowStorageWrapper { + constructor(prefix?: string) { + super(window.sessionStorage, prefix); + } +} diff --git a/src/storage/wrapper.ts b/src/storage/wrapper.ts new file mode 100644 index 0000000..2dbc702 --- /dev/null +++ b/src/storage/wrapper.ts @@ -0,0 +1,24 @@ +import { CacheStorage, StorageValue } from './types'; +/** + * A storage that uses any {@link Storage} as his storage. + */ +export abstract class WindowStorageWrapper implements CacheStorage { + constructor( + readonly storage: Storage, + readonly prefix: string = 'axios-cache:' + ) {} + + get = async (key: string): Promise => { + const json = this.storage.getItem(this.prefix + key); + return json ? JSON.parse(json) : null; + }; + + set = async (key: string, value: StorageValue): Promise => { + const json = JSON.stringify(value); + this.storage.setItem(this.prefix + key, json); + }; + + remove = async (key: string): Promise => { + this.storage.removeItem(this.prefix + key); + }; +} diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..c498b1f --- /dev/null +++ b/src/test.ts @@ -0,0 +1,16 @@ +import axios from 'axios'; +import { createCache } from './'; +import { SessionCacheStorage } from './storage'; + +// My own api +const api = axios.create(); + +const cache = createCache(api, { + // Store values on window.sessionStorage + storage: new SessionCacheStorage(), + + // Use the max-age header to determina the cache expiration time + interpretHeader: true +}); + +cache.get('http://example.com/');