Skip to content

Commit

Permalink
feat: preserve response status codes and use response.cached
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurfiorette committed Oct 12, 2021
1 parent 65cec04 commit 75deccf
Show file tree
Hide file tree
Showing 20 changed files with 3,534 additions and 3,629 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
},
"homepage": "https://github.com/ArthurFiorette/axios-cache-interceptor#readme",
"dependencies": {
"@tusbar/cache-control": "^0.6.0"
"@tusbar/cache-control": "^0.6.0",
"typed-core": "^1.3.0"
},
"devDependencies": {
"@arthurfiorette/prettier-config": "^1.0.6",
Expand Down
50 changes: 30 additions & 20 deletions src/axios/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
AxiosResponse,
Method
} from 'axios';
import type { Deferred } from 'typed-core/dist/promises/deferred';
import type { HeaderInterpreter } from '../header/types';
import type { AxiosInterceptor } from '../interceptors/types';
import type {
Expand All @@ -14,7 +15,6 @@ import type {
CacheStorage,
EmptyStorageValue
} from '../storage/types';
import type { Deferred } from '../util/deferred';
import type { CachePredicate, KeyGenerator } from '../util/types';

export type CacheUpdater =
Expand All @@ -32,9 +32,8 @@ export type CacheProperties = {
/**
* The time until the cached value is expired in milliseconds.
*
* When using `interpretHeader: true`, this value will only
* be used if the interpreter can't determine their TTL value
* to override this
* When using `interpretHeader: true`, this value will only be used
* if the interpreter can't determine their TTL value to override this
*
* **Note**: a custom storage implementation may not respect this.
*
Expand Down Expand Up @@ -81,20 +80,31 @@ export type CacheProperties = {
update: Record<string, CacheUpdater>;
};

export type CacheAxiosResponse<T = never> = AxiosResponse<T> & {
config: CacheRequestConfig;
/**
* @template T The data type that this responses use. Also the same
* generic type as it's request
*/
export type CacheAxiosResponse<T> = AxiosResponse<T> & {
config: CacheRequestConfig<T>;

/**
* The id used for this request. if config specified an id, the id
* will be returned
*/
id: string;

/**
* A simple boolean to check whether this request was cached or not
*/
cached: boolean;
};

/**
* Options that can be overridden per request
*
* @template T The data that this request should return
*/
export type CacheRequestConfig = AxiosRequestConfig & {
export type CacheRequestConfig<T> = AxiosRequestConfig<T> & {
/**
* An id for this request, if this request is used in cache, only
* the last request made with this id will be returned.
Expand Down Expand Up @@ -144,7 +154,7 @@ export interface CacheInstance {
/**
* The request interceptor that will be used to handle the cache.
*/
requestInterceptor: AxiosInterceptor<CacheRequestConfig>;
requestInterceptor: AxiosInterceptor<CacheRequestConfig<any>>;

/**
* The response interceptor that will be used to handle the cache.
Expand All @@ -161,49 +171,49 @@ export interface CacheInstance {
* @see CacheInstance
*/
export interface AxiosCacheInstance extends AxiosInstance, CacheInstance {
(config: CacheRequestConfig): AxiosPromise;
(url: string, config?: CacheRequestConfig): AxiosPromise;
<T>(config: CacheRequestConfig<T>): AxiosPromise;
<T>(url: string, config?: CacheRequestConfig<T>): AxiosPromise;

defaults: DefaultCacheRequestConfig;

interceptors: {
request: AxiosInterceptorManager<CacheRequestConfig>;
request: AxiosInterceptorManager<CacheRequestConfig<any>>;
response: AxiosInterceptorManager<CacheAxiosResponse<never>>;
};

getUri(config?: CacheRequestConfig): string;
getUri<T>(config?: CacheRequestConfig<T>): string;

request<T = any, R = CacheAxiosResponse<T>>(config: CacheRequestConfig): Promise<R>;
request<T = any, R = CacheAxiosResponse<T>>(config: CacheRequestConfig<T>): Promise<R>;

get<T = any, R = CacheAxiosResponse<T>>(
url: string,
config?: CacheRequestConfig
config?: CacheRequestConfig<T>
): Promise<R>;
delete<T = any, R = CacheAxiosResponse<T>>(
url: string,
config?: CacheRequestConfig
config?: CacheRequestConfig<T>
): Promise<R>;
head<T = any, R = CacheAxiosResponse<T>>(
url: string,
config?: CacheRequestConfig
config?: CacheRequestConfig<T>
): Promise<R>;
options<T = any, R = CacheAxiosResponse<T>>(
url: string,
config?: CacheRequestConfig
config?: CacheRequestConfig<T>
): Promise<R>;
post<T = any, R = CacheAxiosResponse<T>>(
url: string,
data?: any,
config?: CacheRequestConfig
config?: CacheRequestConfig<T>
): Promise<R>;
put<T = any, R = CacheAxiosResponse<T>>(
url: string,
data?: any,
config?: CacheRequestConfig
config?: CacheRequestConfig<T>
): Promise<R>;
patch<T = any, R = CacheAxiosResponse<T>>(
url: string,
data?: any,
config?: CacheRequestConfig
config?: CacheRequestConfig<T>
): Promise<R>;
}
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ export * from './axios/types';
export * from './header/types';
export * from './interceptors/types';
export * from './storage/types';
export * as StatusCodes from './util/status-codes';
export * from './util/types';
39 changes: 25 additions & 14 deletions src/interceptors/request.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import type { AxiosCacheInstance, CacheRequestConfig } from '../axios/types';
import { deferred } from 'typed-core/dist/promises/deferred';
import type {
AxiosCacheInstance,
CacheAxiosResponse,
CacheRequestConfig
} from '../axios/types';
import type {
CachedResponse,
CachedStorageValue,
LoadingStorageValue
} from '../storage/types';
import { deferred } from '../util/deferred';
import { CACHED_STATUS_CODE, CACHED_STATUS_TEXT } from '../util/status-codes';
import type { AxiosInterceptor } from './types';

export class CacheRequestInterceptor implements AxiosInterceptor<CacheRequestConfig> {
export class CacheRequestInterceptor<R>
implements AxiosInterceptor<CacheRequestConfig<R>>
{
constructor(readonly axios: AxiosCacheInstance) {}

use = (): void => {
this.axios.interceptors.request.use(this.onFulfilled);
};

onFulfilled = async (config: CacheRequestConfig): Promise<CacheRequestConfig> => {
onFulfilled = async (config: CacheRequestConfig<R>): Promise<CacheRequestConfig<R>> => {
// Skip cache
if (config.cache === false) {
return config;
Expand Down Expand Up @@ -66,7 +71,7 @@ export class CacheRequestInterceptor implements AxiosInterceptor<CacheRequestCon
return config;
}

let data: CachedResponse = {};
let cachedResponse: CachedResponse;

if (cache.state === 'loading') {
const deferred = this.axios.waiting[key];
Expand All @@ -81,22 +86,28 @@ export class CacheRequestInterceptor implements AxiosInterceptor<CacheRequestCon
}

try {
data = await deferred;
cachedResponse = await deferred;
} catch (e) {
// The deferred is rejected when the request that we are waiting rejected cache.
return config;
}
} else {
data = cache.data;
cachedResponse = cache.data;
}

config.adapter = () =>
Promise.resolve({
config,
data: data.body,
headers: data.headers,
status: CACHED_STATUS_CODE,
statusText: CACHED_STATUS_TEXT
/**
* Even though the response interceptor receives this one from
* here, it has been configured to ignore cached responses: true
*/
Promise.resolve<CacheAxiosResponse<R>>({
config: config,
data: cachedResponse.data,
headers: cachedResponse.headers,
status: cachedResponse.status,
statusText: cachedResponse.statusText,
cached: true,
id: key
});

return config;
Expand Down
40 changes: 28 additions & 12 deletions src/interceptors/response.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import type { AxiosResponse } from 'axios';
import { extract } from 'typed-core/dist/core/object';
import type {
AxiosCacheInstance,
CacheAxiosResponse,
CacheProperties,
CacheRequestConfig
CacheProperties
} from '../axios/types';
import type { CachedStorageValue } from '../storage/types';
import { checkPredicateObject } from '../util/cache-predicate';
import { updateCache } from '../util/update-cache';
import type { AxiosInterceptor } from './types';

type CacheConfig = CacheRequestConfig & { cache?: Partial<CacheProperties> };

export class CacheResponseInterceptor<R>
implements AxiosInterceptor<CacheAxiosResponse<R>>
{
Expand All @@ -23,7 +21,7 @@ export class CacheResponseInterceptor<R>

private testCachePredicate = <R>(
response: AxiosResponse<R>,
{ cache }: CacheConfig
cache?: Partial<CacheProperties>
): boolean => {
const cachePredicate =
cache?.cachePredicate || this.axios.defaults.cache.cachePredicate;
Expand All @@ -48,25 +46,41 @@ export class CacheResponseInterceptor<R>
};

onFulfilled = async (
response: CacheAxiosResponse<R>
axiosResponse: AxiosResponse<R>
): Promise<CacheAxiosResponse<R>> => {
const key = this.axios.generateKey(response.config);
response.id = key;
const key = this.axios.generateKey(axiosResponse.config);

const response: CacheAxiosResponse<R> = {
id: key,
// When the request interceptor override the request adapter, it means
// that the response.cached will be true and therefore, the request was cached.
cached: (axiosResponse as CacheAxiosResponse<R>).cached || false,
...axiosResponse
};

// Skip cache
if (response.config.cache === false) {
return { ...response, cached: false };
}

// Response was marked as cached
if (response.cached) {
return response;
}

const cache = await this.axios.storage.get(key);

// Response shouldn't be cached or was already cached
/**
* From now on, the cache and response represents the state of the
* first response to a request, which has not yet been cached or
* processed before.
*/
if (cache.state !== 'loading') {
return response;
}

// Config told that this response should be cached.
if (!this.testCachePredicate(response, response.config as CacheConfig)) {
if (!this.testCachePredicate(response, response.config.cache)) {
await this.rejectResponse(key);
return response;
}
Expand All @@ -86,10 +100,10 @@ export class CacheResponseInterceptor<R>
}

const newCache: CachedStorageValue = {
data: { body: response.data, headers: response.headers },
state: 'cached',
ttl: ttl,
createdAt: Date.now()
createdAt: Date.now(),
data: extract(response, ['data', 'headers', 'status', 'statusText'])
};

// Update other entries before updating himself
Expand All @@ -103,8 +117,10 @@ export class CacheResponseInterceptor<R>
await deferred?.resolve(newCache.data);
delete this.axios.waiting[key];

// Define this key as cache on the storage
await this.axios.storage.set(key, newCache);

// Return the response with cached as false, because it was not cached at all
return response;
};
}
6 changes: 4 additions & 2 deletions src/storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ export interface CacheStorage {
}

export type CachedResponse = {
headers?: any;
body?: any;
data?: any;
headers: Record<string, string>;
status: number;
statusText: string;
};

/**
Expand Down
8 changes: 4 additions & 4 deletions src/util/cache-predicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { CachePredicateObject } from './types';

export function checkPredicateObject<R>(
response: AxiosResponse<R>,
{ statusCheck, containsHeaders: containsHeader, responseMatch }: CachePredicateObject
{ statusCheck, containsHeaders, responseMatch }: CachePredicateObject
): boolean {
if (statusCheck) {
if (typeof statusCheck === 'function') {
Expand All @@ -21,10 +21,10 @@ export function checkPredicateObject<R>(
}
}

if (containsHeader) {
for (const headerName in containsHeader) {
if (containsHeaders) {
for (const headerName in containsHeaders) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const value = containsHeader[headerName]!;
const value = containsHeaders[headerName]!;
const header = response.headers[headerName];

// At any case, if the header is not found, the predicate fails.
Expand Down
26 changes: 0 additions & 26 deletions src/util/deferred.ts

This file was deleted.

2 changes: 0 additions & 2 deletions src/util/status-codes.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/util/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ export type CachePredicateObject = {
* 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 = <R>(options: CacheRequestConfig<R>) => string;

0 comments on commit 75deccf

Please sign in to comment.