UNPKG

@hyper-fetch/core

Version:

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

662 lines (594 loc) 21.4 kB
/* eslint-disable max-lines */ import { RequestSendOptionsType, ParamsType, RequestSendType, PayloadType, RequestJSON, RequestOptionsType, sendRequest, RequestConfigurationType, PayloadMapperType, RequestInstance, RequestMapper, ResponseMapper, ExtractUrlParams, } from "request"; import { ClientInstance } from "client"; import { ResponseErrorType, ResponseSuccessType, ResponseType } from "adapter"; import { ExtractAdapterType, ExtractClientAdapterType, ExtractClientGlobalError, ExtractEndpointType, ExtractParamsType, ExtractPayloadType, ExtractQueryParamsType, EmptyTypes, ExtractAdapterMethodType, ExtractAdapterOptionsType, HydrateDataType, SyncOrAsync, } from "types"; import { Time } from "constants/time.constants"; import { MockerConfigType, MockResponseType } from "mocker"; /** * Request is a class that represents a request sent to the server. It contains all the necessary information to make a request, like endpoint, method, headers, data, and much more. * It is executed at any time via methods like `send` or `exec`. * * We can set it up with options like endpoint, method, headers and more. * We can choose some of advanced settings like cache, invalidation patterns, concurrency, retries and much, much more. * * @info We should not use this class directly in the standard development flow. * We can initialize it using the `createRequest` method on the **Client** class. * * @attention The most important thing about the request is that it keeps data in the format that can be dumped. * This is necessary for the persistence and different dispatcher storage types. * This class doesn't have any callback methods by design and communicate with dispatcher and cache by events. * * It should be serializable to JSON and deserializable back to the class. * Serialization should not affect the result of the request, so it's methods and functional part should be only syntax sugar for given runtime. */ export class Request< Response, Payload, QueryParams, LocalError, Endpoint extends string, Client extends ClientInstance, HasPayload extends true | false = false, HasParams extends true | false = false, HasQuery extends true | false = false, > { endpoint: Endpoint; headers?: HeadersInit; auth: boolean; method: ExtractAdapterMethodType<ExtractClientAdapterType<Client>>; params: ExtractUrlParams<Endpoint> | EmptyTypes; payload: PayloadType<Payload>; queryParams: QueryParams | EmptyTypes; options?: ExtractAdapterOptionsType<ExtractClientAdapterType<Client>> | undefined; cancelable: boolean; retry: number; retryTime: number; cacheTime: number; cache: boolean; staleTime: number; queued: boolean; offline: boolean; abortKey: string; cacheKey: string; queryKey: string; used: boolean; deduplicate: boolean; deduplicateTime: number | null; isMockerEnabled = false; unstable_mock?: { fn: (options: { request: RequestInstance; requestId: string; }) => MockResponseType<Response, LocalError | ExtractClientGlobalError<Client>, ExtractClientAdapterType<Client>>; config: MockerConfigType; }; /** @internal */ unstable_payloadMapper?: PayloadMapperType<Payload>; /** @internal */ unstable_requestMapper?: RequestMapper<any, any>; /** @internal */ unstable_responseMapper?: ResponseMapper<this, ResponseSuccessType<any, any> | ResponseErrorType<any, any>>; unstable_hasParams: HasParams = false as HasParams; unstable_hasPayload: HasPayload = false as HasPayload; unstable_hasQuery: HasQuery = false as HasQuery; private updatedAbortKey: boolean; private updatedCacheKey: boolean; private updatedQueryKey: boolean; constructor( readonly client: Client, readonly requestOptions: RequestOptionsType< Endpoint, ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>, ExtractAdapterMethodType<ExtractClientAdapterType<Client>> >, readonly initialRequestConfiguration?: | RequestConfigurationType< Payload, Endpoint extends string ? ExtractUrlParams<Endpoint> : never, QueryParams, Endpoint, ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>, ExtractAdapterMethodType<ExtractClientAdapterType<Client>> > | undefined, ) { const configuration: RequestOptionsType< Endpoint, ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>, ExtractAdapterMethodType<ExtractClientAdapterType<Client>> > = { ...(this.client.adapter.unstable_getRequestDefaults?.(requestOptions) as RequestOptionsType< Endpoint, ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>, ExtractAdapterMethodType<ExtractClientAdapterType<Client>> >), ...requestOptions, }; const { endpoint, headers, auth = true, method = client.adapter.defaultMethod, options, cancelable = false, retry = 0, retryTime = 500, cacheTime = Time.MIN * 5, cache = true, staleTime = Time.MIN * 5, queued = false, offline = true, abortKey, cacheKey, queryKey, deduplicate = false, deduplicateTime = null, } = configuration; this.endpoint = initialRequestConfiguration?.endpoint ?? endpoint; this.headers = initialRequestConfiguration?.headers ?? headers; this.auth = initialRequestConfiguration?.auth ?? auth; this.method = method as ExtractAdapterMethodType<ExtractClientAdapterType<Client>>; this.params = initialRequestConfiguration?.params; this.payload = initialRequestConfiguration?.payload; this.queryParams = initialRequestConfiguration?.queryParams; this.options = initialRequestConfiguration?.options ?? options; this.cancelable = initialRequestConfiguration?.cancelable ?? cancelable; this.retry = initialRequestConfiguration?.retry ?? retry; this.retryTime = initialRequestConfiguration?.retryTime ?? retryTime; this.cacheTime = initialRequestConfiguration?.cacheTime ?? cacheTime; this.cache = initialRequestConfiguration?.cache ?? cache; this.staleTime = initialRequestConfiguration?.staleTime ?? staleTime; this.queued = initialRequestConfiguration?.queued ?? queued; this.offline = initialRequestConfiguration?.offline ?? offline; this.abortKey = initialRequestConfiguration?.abortKey ?? abortKey ?? this.client.unstable_abortKeyMapper(this); this.cacheKey = initialRequestConfiguration?.cacheKey ?? cacheKey ?? this.client.unstable_cacheKeyMapper(this); this.queryKey = initialRequestConfiguration?.queryKey ?? queryKey ?? this.client.unstable_queryKeyMapper(this); this.used = initialRequestConfiguration?.used ?? false; this.deduplicate = initialRequestConfiguration?.deduplicate ?? deduplicate; this.deduplicateTime = initialRequestConfiguration?.deduplicateTime ?? deduplicateTime; this.updatedAbortKey = initialRequestConfiguration?.updatedAbortKey ?? false; this.updatedCacheKey = initialRequestConfiguration?.updatedCacheKey ?? false; this.updatedQueryKey = initialRequestConfiguration?.updatedQueryKey ?? false; } public setHeaders = (headers: HeadersInit) => { return this.clone({ headers }); }; public setAuth = (auth: boolean) => { return this.clone({ auth }); }; public setParams = <P extends ExtractParamsType<this>>(params: P) => { return this.clone<HasPayload, P extends null ? false : true, HasQuery>({ params }); }; public setPayload = <P extends Payload>(payload: P) => { return this.clone<P extends null ? false : true, HasParams, HasQuery>({ payload, }); }; public setQueryParams = (queryParams: QueryParams) => { return this.clone<HasPayload, HasParams, true>({ queryParams }); }; public setOptions = (options: ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>) => { return this.clone<HasPayload, HasParams, true>({ options }); }; public setCancelable = (cancelable: boolean) => { return this.clone({ cancelable }); }; public setRetry = ( retry: RequestOptionsType< Endpoint, ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>, ExtractAdapterMethodType<ExtractClientAdapterType<Client>> >["retry"], ) => { return this.clone({ retry }); }; public setRetryTime = ( retryTime: RequestOptionsType< Endpoint, ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>, ExtractAdapterMethodType<ExtractClientAdapterType<Client>> >["retryTime"], ) => { return this.clone({ retryTime }); }; public setCacheTime = ( cacheTime: RequestOptionsType< Endpoint, ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>, ExtractAdapterMethodType<ExtractClientAdapterType<Client>> >["cacheTime"], ) => { return this.clone({ cacheTime }); }; public setCache = ( cache: RequestOptionsType< Endpoint, ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>, ExtractAdapterMethodType<ExtractClientAdapterType<Client>> >["cache"], ) => { return this.clone({ cache }); }; public setStaleTime = ( staleTime: RequestOptionsType< Endpoint, ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>, ExtractAdapterMethodType<ExtractClientAdapterType<Client>> >["staleTime"], ) => { return this.clone({ staleTime }); }; public setQueued = (queued: boolean) => { return this.clone({ queued }); }; public setAbortKey = (abortKey: string) => { this.updatedAbortKey = true; return this.clone({ abortKey }); }; public setCacheKey = (cacheKey: string) => { this.updatedCacheKey = true; return this.clone({ cacheKey }); }; public setQueryKey = (queryKey: string) => { this.updatedQueryKey = true; return this.clone({ queryKey }); }; public setDeduplicate = (deduplicate: boolean) => { return this.clone({ deduplicate }); }; public setDeduplicateTime = (deduplicateTime: number) => { return this.clone({ deduplicateTime }); }; public setUsed = (used: boolean) => { return this.clone({ used }); }; public setOffline = (offline: boolean) => { return this.clone({ offline }); }; public setMock = ( fn: (options: { request: Request<Response, Payload, QueryParams, LocalError, Endpoint, Client, HasPayload, HasParams, HasQuery>; requestId: string; }) => SyncOrAsync< MockResponseType<Response, LocalError | ExtractClientGlobalError<Client>, ExtractClientAdapterType<Client>> >, config: MockerConfigType = {}, ) => { this.unstable_mock = { fn, config } as typeof this.unstable_mock; this.isMockerEnabled = true; return this; }; public clearMock = () => { this.unstable_mock = undefined; this.isMockerEnabled = false; return this; }; public setMockingEnabled = (isMockerEnabled: boolean) => { this.isMockerEnabled = isMockerEnabled; return this; }; /** * Map data before it gets send to the server * @param payloadMapper * @returns */ public setPayloadMapper = <MappedPayload extends any | Promise<any>>( payloadMapper: (data: Payload) => MappedPayload, ) => { const cloned = this.clone<HasPayload, HasParams, HasQuery>(undefined); cloned.unstable_payloadMapper = payloadMapper as typeof this.unstable_payloadMapper; return cloned; }; /** * Map request before it gets send to the server * @param requestMapper mapper of the request * @returns new request */ public setRequestMapper = <NewRequest extends RequestInstance>(requestMapper: RequestMapper<this, NewRequest>) => { const cloned = this.clone<HasPayload, HasParams, HasQuery>(undefined); cloned.unstable_requestMapper = requestMapper; return cloned; }; /** * Map the response to the new interface * @param responseMapper our mapping callback * @returns new response */ public setResponseMapper = <MappedResponse extends ResponseSuccessType<any, any> | ResponseErrorType<any, any>>( responseMapper?: ResponseMapper<this, MappedResponse>, ) => { const cloned = this.clone<HasPayload, HasParams, HasQuery>(); cloned.unstable_responseMapper = responseMapper; return cloned as unknown as Request< MappedResponse extends ResponseType<infer R, any, any> ? R : Response, Payload, QueryParams, MappedResponse extends ResponseType<any, infer E, any> ? E : LocalError, Endpoint, Client, HasPayload, HasParams, HasQuery >; }; private paramsMapper = (params: ParamsType | null | undefined): Endpoint => { const { endpoint } = this.requestOptions; let stringEndpoint = String(endpoint); if (params) { Object.entries(params).forEach(([key, value]) => { stringEndpoint = stringEndpoint.replace(new RegExp(`:${key}`, "g"), String(value)); }); } return stringEndpoint as Endpoint; }; public toJSON(): RequestJSON<this> { return { requestOptions: this.requestOptions as unknown as RequestOptionsType< ExtractEndpointType<this>, ExtractAdapterOptionsType<ExtractAdapterType<this>>, ExtractAdapterMethodType<ExtractAdapterType<this>> >, endpoint: this.endpoint as ExtractEndpointType<this>, headers: this.headers, auth: this.auth, // TODO: fix this type method: this.method as any, params: this.params as ExtractParamsType<this>, payload: this.payload as ExtractPayloadType<this>, queryParams: this.queryParams as ExtractQueryParamsType<this>, options: this.options, cancelable: this.cancelable, retry: this.retry, retryTime: this.retryTime, cacheTime: this.cacheTime, cache: this.cache, staleTime: this.staleTime, queued: this.queued, offline: this.offline, abortKey: this.abortKey, cacheKey: this.cacheKey, queryKey: this.queryKey, used: this.used, disableResponseInterceptors: this.requestOptions.disableResponseInterceptors, disableRequestInterceptors: this.requestOptions.disableRequestInterceptors, updatedAbortKey: this.updatedAbortKey, updatedCacheKey: this.updatedCacheKey, updatedQueryKey: this.updatedQueryKey, deduplicate: this.deduplicate, deduplicateTime: this.deduplicateTime, isMockerEnabled: this.isMockerEnabled, hasMock: !!this.unstable_mock, }; } public clone< NewData extends true | false = HasPayload, NewParams extends true | false = HasParams, NewQueryParams extends true | false = HasQuery, >( configuration?: RequestConfigurationType< Payload, (typeof this)["params"], QueryParams, Endpoint, ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>, ExtractAdapterMethodType<ExtractClientAdapterType<Client>> >, ) { const json = this.toJSON(); const initialRequestConfiguration: RequestConfigurationType< Payload, Endpoint extends string ? ExtractUrlParams<Endpoint> : never, QueryParams, Endpoint, ExtractAdapterOptionsType<ExtractClientAdapterType<Client>>, ExtractAdapterMethodType<ExtractClientAdapterType<Client>> > = { ...json, ...configuration, options: configuration?.options || this.options, abortKey: this.updatedAbortKey ? configuration?.abortKey || this.abortKey : undefined, cacheKey: this.updatedCacheKey ? configuration?.cacheKey || this.cacheKey : undefined, queryKey: this.updatedQueryKey ? configuration?.queryKey || this.queryKey : undefined, endpoint: this.paramsMapper(configuration?.params || this.params), queryParams: configuration?.queryParams || this.queryParams, payload: configuration?.payload || this.payload, params: (configuration?.params || this.params) as | EmptyTypes | (Endpoint extends string ? ExtractUrlParams<Endpoint> : never), }; const cloned = new Request< Response, Payload, QueryParams, LocalError, Endpoint, Client, NewData, NewParams, NewQueryParams >(this.client, this.requestOptions, initialRequestConfiguration); // Inherit methods cloned.unstable_payloadMapper = this.unstable_payloadMapper; cloned.unstable_responseMapper = this.unstable_responseMapper as typeof cloned.unstable_responseMapper; cloned.unstable_requestMapper = this.unstable_requestMapper; cloned.unstable_mock = this.unstable_mock; cloned.isMockerEnabled = this.isMockerEnabled; return cloned; } public abort = () => { const { requestManager } = this.client; requestManager.abortByKey(this.abortKey); return this.clone(); }; public dehydrate = (config?: { /** in case of using adapter without cache we can provide response to dehydrate */ response?: ResponseType<Response, LocalError | ExtractClientGlobalError<Client>, ExtractClientAdapterType<Client>>; /** override cache data */ override?: boolean; }): | HydrateDataType<Response, LocalError | ExtractClientGlobalError<Client>, ExtractClientAdapterType<Client>> | undefined => { const { response, override = true } = config || {}; if (response) { return { override, cacheTime: this.cacheTime, staleTime: this.staleTime, cacheKey: this.cacheKey, timestamp: +new Date(), hydrated: true, cache: true, response, }; } const cacheData = this.client.cache.get<Response, LocalError | ExtractClientGlobalError<Client>>(this.cacheKey); if (!cacheData) { return undefined; } return { override, cacheTime: this.cacheTime, staleTime: this.staleTime, cacheKey: this.cacheKey, timestamp: +new Date(), hydrated: true, cache: true, response: { data: cacheData.data, error: cacheData.error, status: cacheData.status, success: cacheData.success, extra: cacheData.extra, requestTimestamp: cacheData.requestTimestamp, responseTimestamp: cacheData.responseTimestamp, }, }; }; /** * Read the response from cache data * * If it returns error and data at the same time, it means that latest request was failed * and we show previous data from cache together with error received from actual request */ public read(): | ResponseType<Response, LocalError | ExtractClientGlobalError<Client>, ExtractClientAdapterType<Client>> | undefined { const cacheData = this.client.cache.get<Response, LocalError | ExtractClientGlobalError<Client>>(this.cacheKey); if (cacheData) { return { data: cacheData.data, error: cacheData.error, status: cacheData.status, success: cacheData.success, extra: cacheData.extra, requestTimestamp: cacheData.requestTimestamp, responseTimestamp: cacheData.responseTimestamp, }; } return undefined; } /** * Method to use the request WITHOUT adding it to cache and queues. This mean it will make simple request without queue side effects. * @param options * @disableReturns * @returns * ```tsx * Promise<[Data | null, Error | null, HttpStatus]> * ``` */ public exec: RequestSendType<this> = async (options?: RequestSendOptionsType<this>) => { const { adapter, requestManager } = this.client; const request = this.clone(options); const requestId = this.client.unstable_requestIdMapper(this); // Listen for aborting requestManager.addAbortController(this.abortKey, requestId); const response = await adapter.fetch(request, requestId); // Stop listening for aborting requestManager.removeAbortController(this.abortKey, requestId); if (request.unstable_responseMapper) { return request.unstable_responseMapper(response); } return response; }; /** * Method used to perform requests with usage of cache and queues * @param options * @param requestCallback * @disableReturns * @returns * ```tsx * Promise<[Data | null, Error | null, HttpStatus]> * ``` */ public send: RequestSendType<this> = async (options?: RequestSendOptionsType<this>) => { const { dispatcherType, ...configuration } = options || {}; const request = this.clone(configuration); return sendRequest(request as unknown as this, options); }; static fromJSON = < NewResponse, NewPayload, NewQueryParams, NewLocalError, NewEndpoint extends string, NewClient extends ClientInstance, NewHasPayload extends true | false = false, NewHasParams extends true | false = false, NewHasQuery extends true | false = false, >( client: NewClient, json: RequestJSON< Request< NewResponse, NewPayload, NewQueryParams, NewLocalError, NewEndpoint, NewClient, NewHasPayload, NewHasParams, NewHasQuery > >, ) => { return new Request< NewResponse, NewPayload, NewQueryParams, NewLocalError, NewEndpoint, NewClient, NewHasPayload, NewHasParams, NewHasQuery >(client, json.requestOptions, json); }; }