UNPKG

@hyper-fetch/core

Version:

Cache, Queue and Persist your requests no matter if you are online or offline!

502 lines (443 loc) 17.1 kB
import { adapter as defaultAdapter, AdapterInstance, AdapterPayloadMappingType, AdapterType, ExtractAdapterExtraType, ExtractAdapterMethodType, ExtractAdapterOptionsType, ExtractAdapterQueryParamsType, ExtractUnionAdapter, HeaderMappingType, QueryStringifyOptionsType, ResponseReturnType, xhrExtra, ExtractAdapterEndpointType, } from "adapter"; import { ClientErrorType, ClientInstance, ClientOptionsType, DefaultEndpointMapper, getAdapterHeaders, getAdapterPayload, RequestInterceptorType, ResponseInterceptorType, StringifyCallbackType, stringifyQueryParams, } from "client"; import { Cache } from "cache"; import { Dispatcher } from "dispatcher"; import { RequestEffectInstance } from "effect"; import { getRequestKey, getSimpleKey, Request, RequestInstance, RequestOptionsType } from "request"; import { AppManager, LoggerManager, RequestManager, SeverityType } from "managers"; import { interceptRequest, interceptResponse } from "./client.utils"; import { HttpMethodsEnum } from "../constants/http.constants"; import { ExtractAdapterType, NegativeTypes } from "types"; /** * **Client** is a class that allows you to configure the connection with the server and then use it to create * requests which, when called using the appropriate method, will cause the server to be queried for the endpoint and * method specified in the request. */ export class Client< GlobalErrorType extends ClientErrorType = Error, Adapter extends AdapterInstance = AdapterType, EndpointMapper extends DefaultEndpointMapper = DefaultEndpointMapper, > { readonly url: string; public debug: boolean; // Private __onErrorCallbacks: ResponseInterceptorType[] = []; __onSuccessCallbacks: ResponseInterceptorType[] = []; __onResponseCallbacks: ResponseInterceptorType[] = []; __onAuthCallbacks: RequestInterceptorType[] = []; __onRequestCallbacks: RequestInterceptorType[] = []; // Managers requestManager: RequestManager = new RequestManager(); appManager: AppManager; loggerManager: LoggerManager = new LoggerManager(this); // Config adapter: Adapter; cache: Cache<this>; fetchDispatcher: Dispatcher; submitDispatcher: Dispatcher; defaultMethod: ExtractAdapterMethodType<Adapter> = HttpMethodsEnum.get as ExtractAdapterMethodType<Adapter>; defaultExtra: ExtractAdapterExtraType<Adapter> = xhrExtra as ExtractAdapterExtraType<Adapter>; isMockEnabled = true; // Registered requests effect effects: RequestEffectInstance[] = []; // Options queryParamsConfig?: QueryStringifyOptionsType; adapterDefaultOptions?: (request: RequestInstance) => ExtractAdapterOptionsType<Adapter>; requestDefaultOptions?: ( options: RequestOptionsType<string, ExtractAdapterOptionsType<Adapter>, ExtractAdapterMethodType<Adapter>>, ) => Partial<RequestOptionsType<string, ExtractAdapterOptionsType<Adapter>, ExtractAdapterMethodType<Adapter>>>; abortKeyMapper?: (request: RequestInstance) => string = getSimpleKey; cacheKeyMapper?: (request: RequestInstance) => string = getRequestKey; queueKeyMapper?: (request: RequestInstance) => string = getRequestKey; effectKeyMapper?: (request: RequestInstance) => string = getSimpleKey; // Utils /** * Method to stringify query params from objects. */ stringifyQueryParams: StringifyCallbackType = (queryParams) => stringifyQueryParams(queryParams, this.queryParamsConfig); /** * Method to get default headers and to map them based on the data format exchange, by default it handles FormData / JSON formats. */ headerMapper: HeaderMappingType = getAdapterHeaders; /** * Method to get request data and transform them to the required format. It handles FormData and JSON by default. */ payloadMapper: AdapterPayloadMappingType = getAdapterPayload; /** * Method to get request data and transform them to the required format. It handles FormData and JSON by default. */ // eslint-disable-next-line class-methods-use-this endpointMapper: EndpointMapper = ((endpoint) => endpoint) as any; // Logger logger = this.loggerManager.init("Client"); constructor(public options: ClientOptionsType<Client<GlobalErrorType, Adapter, EndpointMapper>>) { const { url, adapter, appManager, cache, fetchDispatcher, submitDispatcher } = this.options; this.url = url; this.adapter = (adapter || defaultAdapter) as Adapter; // IMPORTANT: Do not change initialization order as it's crucial for dependencies injection this.appManager = appManager?.(this) || new AppManager(); this.cache = (cache?.(this) || new Cache(this)) as Cache<this>; this.fetchDispatcher = fetchDispatcher?.(this) || new Dispatcher(this); this.submitDispatcher = submitDispatcher?.(this) || new Dispatcher(this); } /** * This method allows to configure global defaults for the request configuration like method, auth, deduplication etc. */ setRequestDefaultOptions = ( callback: ( request: RequestInstance, ) => Partial<RequestOptionsType<string, ExtractAdapterOptionsType<Adapter>, ExtractAdapterMethodType<Adapter>>>, ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.requestDefaultOptions = callback; return this; }; setAdapterDefaultOptions = ( callback: (request: RequestInstance) => ExtractAdapterOptionsType<Adapter>, ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.adapterDefaultOptions = callback; return this; }; /** * This method enables the logger usage and display the logs in console */ setDebug = (debug: boolean): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.debug = debug; return this; }; /** * Set the logger severity of the messages displayed to the console */ setLoggerSeverity = (severity: SeverityType): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.loggerManager.setSeverity(severity); return this; }; /** * Set the new logger instance to the Client */ setLogger = ( callback: (Client: ClientInstance) => LoggerManager, ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.loggerManager = callback(this); return this; }; /** * Set config for the query params stringify method, we can set here, among others, arrayFormat, skipNull, encode, skipEmptyString and more */ setQueryParamsConfig = ( queryParamsConfig: QueryStringifyOptionsType, ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.queryParamsConfig = queryParamsConfig; return this; }; /** * Set the custom query params stringify method to the Client * @param stringifyFn Custom callback handling query params stringify */ setStringifyQueryParams = (stringifyFn: StringifyCallbackType): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.stringifyQueryParams = stringifyFn; return this; }; /** * Set the custom header mapping function */ setHeaderMapper = (headerMapper: HeaderMappingType): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.headerMapper = headerMapper; return this; }; /** * Set the request payload mapping function which get triggered before request get send */ setPayloadMapper = (payloadMapper: AdapterPayloadMappingType): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.payloadMapper = payloadMapper; return this; }; /** * Set globally if mocking should be enabled or disabled for all client requests. * @param isMockEnabled */ setEnableGlobalMocking = (isMockEnabled: boolean) => { this.isMockEnabled = isMockEnabled; return this; }; /** * Set the request payload mapping function which get triggered before request get send */ setEndpointMapper = <NewEndpointMapper extends DefaultEndpointMapper>(endpointMapper: NewEndpointMapper) => { this.endpointMapper = endpointMapper as any; return this as unknown as Client<GlobalErrorType, Adapter, NewEndpointMapper>; }; /** * Set custom http adapter to handle graphql, rest, firebase or others */ setAdapter = <NewAdapter extends AdapterInstance, Returns extends AdapterInstance | ClientInstance>( callback: ( client: this, ) => Returns extends AdapterInstance ? NewAdapter : Client<GlobalErrorType, NewAdapter, EndpointMapper>, ): Client< GlobalErrorType, Returns extends AdapterInstance ? NewAdapter : ExtractAdapterType<NewAdapter>, EndpointMapper > => { const value = callback(this) as unknown as Adapter; if (value instanceof Client) { return value as any; } this.adapter = value; return this as any; }; /** * Set default method for requests. */ setDefaultMethod = (defaultMethod: ExtractAdapterMethodType<Adapter>) => { this.defaultMethod = defaultMethod; return this as ClientInstance; }; /** * Set default additional data for initial state. */ setDefaultExtra = (defaultExtra: ExtractAdapterExtraType<Adapter>) => { this.defaultExtra = defaultExtra; return this as ClientInstance; }; /** * Method of manipulating requests before sending the request. We can for example add custom header with token to the request which request had the auth set to true. */ onAuth = (callback: RequestInterceptorType): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.__onAuthCallbacks.push(callback); return this; }; /** * Method for removing listeners on auth. * */ removeOnAuthInterceptors = ( callbacks: RequestInterceptorType[], ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.__onAuthCallbacks = this.__onAuthCallbacks.filter((callback) => !callbacks.includes(callback)); return this; }; /** * Method for intercepting error responses. It can be used for example to refresh tokens. */ onError = <ErrorType = null>( callback: ResponseInterceptorType<any, ErrorType | GlobalErrorType, Adapter>, ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.__onErrorCallbacks.push(callback); return this; }; /** * Method for removing listeners on error. * */ removeOnErrorInterceptors = ( callbacks: ResponseInterceptorType<any, null | GlobalErrorType, Adapter>[], ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.__onErrorCallbacks = this.__onErrorCallbacks.filter((callback) => !callbacks.includes(callback)); return this; }; /** * Method for intercepting success responses. */ onSuccess = <ErrorType = null>( callback: ResponseInterceptorType<any, ErrorType | GlobalErrorType, Adapter>, ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.__onSuccessCallbacks.push(callback); return this; }; /** * Method for removing listeners on success. * */ removeOnSuccessInterceptors = ( callbacks: ResponseInterceptorType<any, null | GlobalErrorType, Adapter>[], ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.__onSuccessCallbacks = this.__onSuccessCallbacks.filter((callback) => !callbacks.includes(callback)); return this; }; /** * Method of manipulating requests before sending the request. */ onRequest = (callback: RequestInterceptorType): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.__onRequestCallbacks.push(callback); return this; }; /** * Method for removing listeners on request. * */ removeOnRequestInterceptors = ( callbacks: RequestInterceptorType[], ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.__onRequestCallbacks = this.__onRequestCallbacks.filter((callback) => !callbacks.includes(callback)); return this; }; /** * Method for intercepting any responses. */ onResponse = <ErrorType = null>( callback: ResponseInterceptorType<any, ErrorType | GlobalErrorType, Adapter>, ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.__onResponseCallbacks.push(callback); return this; }; /** * Method for removing listeners on request. * */ removeOnResponseInterceptors = ( callbacks: ResponseInterceptorType<any, null | GlobalErrorType, Adapter>[], ): Client<GlobalErrorType, Adapter, EndpointMapper> => { this.__onResponseCallbacks = this.__onResponseCallbacks.filter((callback) => !callbacks.includes(callback)); return this; }; /** * Add persistent effects which trigger on the request lifecycle */ addEffect = (effect: RequestEffectInstance | RequestEffectInstance[]) => { this.effects = this.effects.concat(effect); return this; }; /** * Remove effects from Client */ removeEffect = (effect: RequestEffectInstance | string) => { const name = typeof effect === "string" ? effect : effect.getEffectKey(); this.effects = this.effects.filter((currentEffect) => currentEffect.getEffectKey() !== name); return this; }; /** * Key setters */ setAbortKeyMapper = (callback: (request: RequestInstance) => string) => { this.abortKeyMapper = callback; }; setCacheKeyMapper = (callback: (request: RequestInstance) => string) => { this.cacheKeyMapper = callback; }; setQueueKeyMapper = (callback: (request: RequestInstance) => string) => { this.queueKeyMapper = callback; }; setEffectKeyMapper = (callback: (request: RequestInstance) => string) => { this.effectKeyMapper = callback; }; /** * Helper used by http adapter to apply the modifications on response error */ __modifyAuth = async (request: RequestInstance) => interceptRequest(this.__onAuthCallbacks, request); /** * Private helper to run async pre-request processing */ __modifyRequest = async (request: RequestInstance) => interceptRequest(this.__onRequestCallbacks, request); /** * Private helper to run async on-error response processing */ __modifyErrorResponse = async ( response: ResponseReturnType<any, GlobalErrorType, Adapter>, request: RequestInstance, ) => interceptResponse<GlobalErrorType, Adapter>(this.__onErrorCallbacks, response, request); /** * Private helper to run async on-success response processing */ __modifySuccessResponse = async ( response: ResponseReturnType<any, GlobalErrorType, Adapter>, request: RequestInstance, ) => interceptResponse<GlobalErrorType, Adapter>(this.__onSuccessCallbacks, response, request); /** * Private helper to run async response processing */ __modifyResponse = async (response: ResponseReturnType<any, GlobalErrorType, Adapter>, request: RequestInstance) => interceptResponse<GlobalErrorType, Adapter>(this.__onResponseCallbacks, response, request); /** * Clears the Client instance and remove all listeners on it's dependencies */ clear = () => { const { appManager, cache, fetchDispatcher, submitDispatcher } = this.options; this.requestManager.abortControllers.clear(); this.fetchDispatcher.clear(); this.submitDispatcher.clear(); this.cache.clear(); this.requestManager.emitter.removeAllListeners(); this.fetchDispatcher.emitter.removeAllListeners(); this.submitDispatcher.emitter.removeAllListeners(); this.cache.emitter.removeAllListeners(); this.appManager = appManager?.(this) || new AppManager(); this.cache = (cache?.(this) || new Cache(this)) as Cache<this>; this.fetchDispatcher = fetchDispatcher?.(this) || new Dispatcher(this); this.submitDispatcher = submitDispatcher?.(this) || new Dispatcher(this); }; /** * Create requests based on the Client setup */ createRequest = < Response, Payload = undefined, LocalError = undefined, QueryParams = ExtractAdapterQueryParamsType<Adapter>, >() => { return < EndpointType extends ExtractAdapterEndpointType<Adapter>, AdapterOptions extends ExtractAdapterOptionsType<Adapter>, MethodType extends ExtractAdapterMethodType<Adapter>, >( params: RequestOptionsType<EndpointType, AdapterOptions, MethodType>, ) => { const endpoint = this.endpointMapper(params.endpoint); type ExtractedAdapterType = ExtractUnionAdapter< Adapter, { method: MethodType; options: AdapterOptions; queryParams: QueryParams; } > extends NegativeTypes ? Adapter : ExtractUnionAdapter< Adapter, { method: MethodType; options: AdapterOptions; queryParams: QueryParams; } >; const mappedParams: RequestOptionsType< EndpointType extends string ? EndpointType : typeof endpoint, AdapterOptions, MethodType > = { ...params, endpoint: endpoint as EndpointType extends string ? EndpointType : typeof endpoint, }; return new Request< Response, Payload, QueryParams, GlobalErrorType, LocalError, EndpointType extends string ? EndpointType : typeof endpoint, ExtractedAdapterType >(this as any, mappedParams); }; }; }