parent
68d379d3c5
commit
7683c20a2e
11 changed files with 152 additions and 37 deletions
|
@ -10,7 +10,7 @@
|
|||
],
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "rm -rf target && tsc",
|
||||
"build": "tsc",
|
||||
"prepublishOnly": "rm -rf target && tsc",
|
||||
"report-coverage": "cat coverage/lcov.info | coveralls"
|
||||
},
|
||||
|
|
|
@ -17,12 +17,17 @@ export type RequestProps<R> = PrepareRequestProps & {
|
|||
transformResponseData?: (data: unknown) => R,
|
||||
}
|
||||
|
||||
export type ClientResponse<Data extends Record<string, any>> = Readonly<
|
||||
export type ResponseWithError =
|
||||
Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'>
|
||||
& { data: Data }
|
||||
>
|
||||
& Readonly<{ error?: Error, canceled?: boolean }>
|
||||
|
||||
export type ClientResponse<Data extends Record<string, any>> =
|
||||
ResponseWithError
|
||||
& Readonly<{ data: Data }>
|
||||
|
||||
export class Client {
|
||||
private controller = new AbortController();
|
||||
|
||||
constructor(private config: ClientConfig) {}
|
||||
|
||||
public prepareRequest(props: PrepareRequestProps) {
|
||||
|
@ -82,10 +87,29 @@ export class Client {
|
|||
): Promise<ClientResponse<Data>> {
|
||||
const req = this.prepareRequest(restProps);
|
||||
|
||||
return fetch(req)
|
||||
return fetch(req, { signal: this.controller.signal })
|
||||
// TODO: need to check response headers and parse json only if content-type header is application/json
|
||||
.then(res => Promise.all([res, res.json(), false]))
|
||||
.then(([res, data]) => {
|
||||
.then(
|
||||
(res) => Promise.all([res, res.json()]),
|
||||
(err) => {
|
||||
const canceled = err.name === 'AbortError';
|
||||
return Promise.all([
|
||||
{
|
||||
ok: false,
|
||||
redirected: false,
|
||||
status: canceled ? 499 : 400,
|
||||
statusText: canceled ? 'Client Closed Request' : err.toString(),
|
||||
type: 'basic',
|
||||
headers: {},
|
||||
url: req.url,
|
||||
error: err,
|
||||
canceled,
|
||||
} as ResponseWithError,
|
||||
{}
|
||||
]);
|
||||
}
|
||||
)
|
||||
.then(([res, data]): ClientResponse<Data> => {
|
||||
return {
|
||||
ok: res.ok,
|
||||
redirected: res.redirected,
|
||||
|
@ -94,6 +118,8 @@ export class Client {
|
|||
type: res.type,
|
||||
headers: res.headers,
|
||||
url: res.url,
|
||||
error: 'error' in res ? res.error : undefined,
|
||||
canceled: 'canceled' in res ? res.canceled : false,
|
||||
data: isFunction(transformResponseData) ? transformResponseData(data) : data,
|
||||
};
|
||||
})
|
||||
|
@ -105,4 +131,9 @@ export class Client {
|
|||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
public cancelRequest() {
|
||||
this.controller.abort();
|
||||
this.controller = new AbortController();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,16 +30,17 @@ export type LazyRequestHandlerConfig<E extends AnyEndpoint> = Readonly<
|
|||
export type RequestHandler<E extends AnyEndpoint> =
|
||||
(config?: LazyRequestHandlerConfig<E>) => Promise<ExtractEndpointResponse<E> | null>;
|
||||
|
||||
export type RefetchRequestHandler = () => void;
|
||||
|
||||
export type PublicRequestStateWithRefetch<E extends AnyEndpoint> =
|
||||
export type PublicRequestStateWithActions<E extends AnyEndpoint> =
|
||||
PublicRequestState<ExtractEndpointResponse<E>>
|
||||
& { refetch: RefetchRequestHandler };
|
||||
& {
|
||||
refetch: () => void,
|
||||
cancel: () => void,
|
||||
};
|
||||
|
||||
export function useLazyRequest<E extends AnyEndpoint>(
|
||||
endpoint: E,
|
||||
config?: LazyRequestConfigFromEndpoint<E>,
|
||||
): [RequestHandler<E>, PublicRequestStateWithRefetch<E>] {
|
||||
): [RequestHandler<E>, PublicRequestStateWithActions<E>] {
|
||||
const [client] = useClient();
|
||||
const { defaultHeaders } = useRequestContext();
|
||||
const [state, dispatch] = React.useReducer<RequestReducer<ExtractEndpointResponse<E>>>(
|
||||
|
@ -63,7 +64,7 @@ export function useLazyRequest<E extends AnyEndpoint>(
|
|||
|
||||
const handler = React.useCallback(
|
||||
(handlerConfig?: LazyRequestHandlerConfig<E>) => {
|
||||
if (state?.loading) {
|
||||
if (state?.loading || state?.isCanceled) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
|
@ -94,7 +95,8 @@ export function useLazyRequest<E extends AnyEndpoint>(
|
|||
};
|
||||
|
||||
if (
|
||||
isSameRequest
|
||||
state.isCalled
|
||||
&& isSameRequest
|
||||
&& state?.prevVariables && isEqual(state.prevVariables, variables)
|
||||
&& state?.prevHeaders && isEqual(state.prevHeaders, headers)
|
||||
&& !handlerConfig?.force
|
||||
|
@ -127,11 +129,13 @@ export function useLazyRequest<E extends AnyEndpoint>(
|
|||
|
||||
return response.data;
|
||||
},
|
||||
(response) => {
|
||||
(response: ClientResponse<ExtractEndpointResponse<E>>) => {
|
||||
dispatch({ type: 'failure', response });
|
||||
if (isFunction(onFailure)) {
|
||||
|
||||
if (!response.canceled && isFunction(onFailure)) {
|
||||
onFailure(response);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
@ -151,13 +155,26 @@ export function useLazyRequest<E extends AnyEndpoint>(
|
|||
[handler, prevHandlerConfig]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
return () => {
|
||||
dispatch({ type: 'cancel' });
|
||||
client.cancelRequest();
|
||||
};
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
return [
|
||||
handler,
|
||||
{
|
||||
data: state.data,
|
||||
loading: state.loading,
|
||||
isCalled: state.isCalled,
|
||||
isCanceled: state.isCanceled,
|
||||
error: state.error,
|
||||
refetch,
|
||||
cancel: client.cancelRequest.bind(client),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ export type PublicRequestState<R> = Readonly<{
|
|||
data: R | null;
|
||||
loading: boolean;
|
||||
isCalled: boolean;
|
||||
isCanceled?: boolean;
|
||||
error?: Error;
|
||||
}>;
|
||||
|
||||
export type RequestState<R> = PublicRequestState<R> & Readonly<{
|
||||
|
@ -27,6 +29,9 @@ export type RequestAction<R> =
|
|||
type: 'failure',
|
||||
response: ClientResponse<R>
|
||||
}
|
||||
| {
|
||||
type: 'cancel'
|
||||
}
|
||||
|
||||
export type RequestReducer<R> = React.Reducer<RequestState<R>, RequestAction<R>>
|
||||
|
||||
|
@ -35,6 +40,8 @@ export function requestReducer<R>(state: RequestState<R>, action: RequestAction<
|
|||
case 'call': {
|
||||
return {
|
||||
...state,
|
||||
error: undefined,
|
||||
isCanceled: false,
|
||||
loading: true,
|
||||
isCalled: true,
|
||||
prevHeaders: action.headers,
|
||||
|
@ -54,7 +61,15 @@ export function requestReducer<R>(state: RequestState<R>, action: RequestAction<
|
|||
...state,
|
||||
loading: false,
|
||||
data: null,
|
||||
// TODO: need to append errors
|
||||
error: action.response.error,
|
||||
isCanceled: action.response.canceled,
|
||||
};
|
||||
}
|
||||
case 'cancel': {
|
||||
return {
|
||||
...state,
|
||||
isCanceled: false,
|
||||
error: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
8
target/client.d.ts
vendored
8
target/client.d.ts
vendored
|
@ -11,13 +11,19 @@ declare type PrepareRequestProps = {
|
|||
export declare type RequestProps<R> = PrepareRequestProps & {
|
||||
transformResponseData?: (data: unknown) => R;
|
||||
};
|
||||
export declare type ClientResponse<Data extends Record<string, any>> = Readonly<Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'> & {
|
||||
export declare type ResponseWithError = Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'> & Readonly<{
|
||||
error?: Error;
|
||||
canceled?: boolean;
|
||||
}>;
|
||||
export declare type ClientResponse<Data extends Record<string, any>> = ResponseWithError & Readonly<{
|
||||
data: Data;
|
||||
}>;
|
||||
export declare class Client {
|
||||
private config;
|
||||
private controller;
|
||||
constructor(config: ClientConfig);
|
||||
prepareRequest(props: PrepareRequestProps): Request;
|
||||
request<Data extends Record<string, any>>({ transformResponseData, ...restProps }: RequestProps<Data>): Promise<ClientResponse<Data>>;
|
||||
cancelRequest(): void;
|
||||
}
|
||||
export {};
|
||||
|
|
|
@ -15,6 +15,7 @@ import { formDataFromObject, isFunction, urlSearchParamsFromObject } from './mis
|
|||
export class Client {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.controller = new AbortController();
|
||||
}
|
||||
prepareRequest(props) {
|
||||
var _a;
|
||||
|
@ -48,9 +49,25 @@ export class Client {
|
|||
request(_a) {
|
||||
var { transformResponseData } = _a, restProps = __rest(_a, ["transformResponseData"]);
|
||||
const req = this.prepareRequest(restProps);
|
||||
return fetch(req)
|
||||
return fetch(req, { signal: this.controller.signal })
|
||||
// TODO: need to check response headers and parse json only if content-type header is application/json
|
||||
.then(res => Promise.all([res, res.json(), false]))
|
||||
.then((res) => Promise.all([res, res.json()]), (err) => {
|
||||
const canceled = err.name === 'AbortError';
|
||||
return Promise.all([
|
||||
{
|
||||
ok: false,
|
||||
redirected: false,
|
||||
status: canceled ? 499 : 400,
|
||||
statusText: canceled ? 'Client Closed Request' : err.toString(),
|
||||
type: 'basic',
|
||||
headers: {},
|
||||
url: req.url,
|
||||
error: err,
|
||||
canceled,
|
||||
},
|
||||
{}
|
||||
]);
|
||||
})
|
||||
.then(([res, data]) => {
|
||||
return {
|
||||
ok: res.ok,
|
||||
|
@ -60,6 +77,8 @@ export class Client {
|
|||
type: res.type,
|
||||
headers: res.headers,
|
||||
url: res.url,
|
||||
error: 'error' in res ? res.error : undefined,
|
||||
canceled: 'canceled' in res ? res.canceled : false,
|
||||
data: isFunction(transformResponseData) ? transformResponseData(data) : data,
|
||||
};
|
||||
})
|
||||
|
@ -70,4 +89,8 @@ export class Client {
|
|||
return res;
|
||||
});
|
||||
}
|
||||
cancelRequest() {
|
||||
this.controller.abort();
|
||||
this.controller = new AbortController();
|
||||
}
|
||||
}
|
||||
|
|
8
target/lazy-request-hook.d.ts
vendored
8
target/lazy-request-hook.d.ts
vendored
|
@ -13,8 +13,8 @@ export declare type LazyRequestHandlerConfig<E extends AnyEndpoint> = Readonly<L
|
|||
force?: boolean;
|
||||
}>;
|
||||
export declare type RequestHandler<E extends AnyEndpoint> = (config?: LazyRequestHandlerConfig<E>) => Promise<ExtractEndpointResponse<E> | null>;
|
||||
export declare type RefetchRequestHandler = () => void;
|
||||
export declare type PublicRequestStateWithRefetch<E extends AnyEndpoint> = PublicRequestState<ExtractEndpointResponse<E>> & {
|
||||
refetch: RefetchRequestHandler;
|
||||
export declare type PublicRequestStateWithActions<E extends AnyEndpoint> = PublicRequestState<ExtractEndpointResponse<E>> & {
|
||||
refetch: () => void;
|
||||
cancel: () => void;
|
||||
};
|
||||
export declare function useLazyRequest<E extends AnyEndpoint>(endpoint: E, config?: LazyRequestConfigFromEndpoint<E>): [RequestHandler<E>, PublicRequestStateWithRefetch<E>];
|
||||
export declare function useLazyRequest<E extends AnyEndpoint>(endpoint: E, config?: LazyRequestConfigFromEndpoint<E>): [RequestHandler<E>, PublicRequestStateWithActions<E>];
|
||||
|
|
|
@ -21,7 +21,7 @@ export function useLazyRequest(endpoint, config) {
|
|||
}, [endpoint]);
|
||||
const handler = React.useCallback((handlerConfig) => {
|
||||
var _a, _b, _c;
|
||||
if (state === null || state === void 0 ? void 0 : state.loading) {
|
||||
if ((state === null || state === void 0 ? void 0 : state.loading) || (state === null || state === void 0 ? void 0 : state.isCanceled)) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
let params;
|
||||
|
@ -38,7 +38,8 @@ export function useLazyRequest(endpoint, config) {
|
|||
}
|
||||
const variables = Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.variables), handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.variables);
|
||||
const headers = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultHeaders), endpoint.headers), config === null || config === void 0 ? void 0 : config.headers), handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.headers);
|
||||
if (isSameRequest
|
||||
if (state.isCalled
|
||||
&& isSameRequest
|
||||
&& (state === null || state === void 0 ? void 0 : state.prevVariables) && isEqual(state.prevVariables, variables)
|
||||
&& (state === null || state === void 0 ? void 0 : state.prevHeaders) && isEqual(state.prevHeaders, headers)
|
||||
&& !(handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.force)) {
|
||||
|
@ -60,7 +61,7 @@ export function useLazyRequest(endpoint, config) {
|
|||
return response.data;
|
||||
}, (response) => {
|
||||
dispatch({ type: 'failure', response });
|
||||
if (isFunction(onFailure)) {
|
||||
if (!response.canceled && isFunction(onFailure)) {
|
||||
onFailure(response);
|
||||
}
|
||||
return null;
|
||||
|
@ -71,13 +72,22 @@ export function useLazyRequest(endpoint, config) {
|
|||
handler(Object.assign(Object.assign({}, prevHandlerConfig), { force: true }));
|
||||
}
|
||||
}, [handler, prevHandlerConfig]);
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
dispatch({ type: 'cancel' });
|
||||
client.cancelRequest();
|
||||
};
|
||||
}, [client]);
|
||||
return [
|
||||
handler,
|
||||
{
|
||||
data: state.data,
|
||||
loading: state.loading,
|
||||
isCalled: state.isCalled,
|
||||
isCanceled: state.isCanceled,
|
||||
error: state.error,
|
||||
refetch,
|
||||
cancel: client.cancelRequest.bind(client),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
24
target/reducer.d.ts
vendored
24
target/reducer.d.ts
vendored
|
@ -4,6 +4,8 @@ export declare type PublicRequestState<R> = Readonly<{
|
|||
data: R | null;
|
||||
loading: boolean;
|
||||
isCalled: boolean;
|
||||
isCanceled?: boolean;
|
||||
error?: Error;
|
||||
}>;
|
||||
export declare type RequestState<R> = PublicRequestState<R> & Readonly<{
|
||||
prevHeaders?: Record<string, string>;
|
||||
|
@ -21,25 +23,33 @@ export declare type RequestAction<R> = {
|
|||
} | {
|
||||
type: 'failure';
|
||||
response: ClientResponse<R>;
|
||||
} | {
|
||||
type: 'cancel';
|
||||
};
|
||||
export declare type RequestReducer<R> = React.Reducer<RequestState<R>, RequestAction<R>>;
|
||||
export declare function requestReducer<R>(state: RequestState<R>, action: RequestAction<R>): {
|
||||
loading: boolean;
|
||||
isCalled: boolean;
|
||||
prevHeaders: Record<string, string>;
|
||||
prevVariables: Record<string, any>;
|
||||
prevParams: Record<string, any> | undefined;
|
||||
data: R | null;
|
||||
} | {
|
||||
loading: boolean;
|
||||
data: R;
|
||||
isCalled: boolean;
|
||||
isCanceled?: boolean | undefined;
|
||||
error?: Error | undefined;
|
||||
prevHeaders?: Record<string, string> | undefined;
|
||||
prevVariables?: Record<string, any> | undefined;
|
||||
prevParams?: Record<string, any> | undefined;
|
||||
} | {
|
||||
loading: boolean;
|
||||
data: null;
|
||||
error: Error | undefined;
|
||||
isCanceled: boolean | undefined;
|
||||
isCalled: boolean;
|
||||
prevHeaders?: Record<string, string> | undefined;
|
||||
prevVariables?: Record<string, any> | undefined;
|
||||
prevParams?: Record<string, any> | undefined;
|
||||
} | {
|
||||
isCanceled: boolean;
|
||||
error: undefined;
|
||||
data: R | null;
|
||||
loading: boolean;
|
||||
isCalled: boolean;
|
||||
prevHeaders?: Record<string, string> | undefined;
|
||||
prevVariables?: Record<string, any> | undefined;
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
export function requestReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'call': {
|
||||
return Object.assign(Object.assign({}, state), { loading: true, isCalled: true, prevHeaders: action.headers, prevVariables: action.variables, prevParams: action.params });
|
||||
return Object.assign(Object.assign({}, state), { error: undefined, isCanceled: false, loading: true, isCalled: true, prevHeaders: action.headers, prevVariables: action.variables, prevParams: action.params });
|
||||
}
|
||||
case 'success': {
|
||||
return Object.assign(Object.assign({}, state), { loading: false, data: action.response.data });
|
||||
}
|
||||
case 'failure': {
|
||||
return Object.assign(Object.assign({}, state), { loading: false, data: null });
|
||||
return Object.assign(Object.assign({}, state), { loading: false, data: null, error: action.response.error, isCanceled: action.response.canceled });
|
||||
}
|
||||
case 'cancel': {
|
||||
return Object.assign(Object.assign({}, state), { isCanceled: false, error: undefined });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
target/request-hook.d.ts
vendored
2
target/request-hook.d.ts
vendored
|
@ -3,4 +3,4 @@ import { LazyRequestConfigFromEndpoint } from './lazy-request-hook';
|
|||
export declare type RequestConfigFromEndpoint<E extends AnyEndpoint> = Readonly<LazyRequestConfigFromEndpoint<E> & {
|
||||
skip?: boolean;
|
||||
}>;
|
||||
export declare function useRequest<E extends AnyEndpoint>(endpoint: E, config?: RequestConfigFromEndpoint<E>): import("./lazy-request-hook").PublicRequestStateWithRefetch<E>;
|
||||
export declare function useRequest<E extends AnyEndpoint>(endpoint: E, config?: RequestConfigFromEndpoint<E>): import("./lazy-request-hook").PublicRequestStateWithActions<E>;
|
||||
|
|
Reference in a new issue