UNPKG

@hyper-fetch/core

Version:

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

502 lines (462 loc) 16.1 kB
/* eslint-disable class-methods-use-this */ import type { EmptyTypes, ExtendRequest } from "types"; import { getAdapterOnError, getAdapterBindings } from "./adapter.bindings"; import type { EndpointMapper, AdapterFetcherType, AdapterPayloadMappingType, HeaderMappingType, QueryParamsMapper, QueryParamsType, RequestResponseType, ResponseType, } from "./adapter.types"; import type { RequestInstance, RequestOptionsType } from "request"; import type { Client, ClientInstance } from "client"; import { mocker } from "mocker"; import type { LoggerMethods } from "managers"; import type { getErrorMessage } from "./adapter.utils"; import { getAdapterHeaders, getAdapterPayload, RequestProcessingError } from "./adapter.utils"; export type DefaultMapperType = <V, C>(value: V, config: C) => V; export const defaultMapper: DefaultMapperType = (value) => value; /** * Base adapter class responsible for executing HTTP requests. Adapters handle endpoint resolution, * header/payload/query-params mapping, and the actual fetch call. Extend or configure this class * to integrate custom transport layers (e.g., axios, graphql, firebase). */ export class Adapter< AdapterOptions, MethodType extends string, StatusType extends number | string, Extra extends Record<string, any>, QueryParams = QueryParamsType | string | EmptyTypes, DefaultQueryParams = undefined, EndpointType = string, EndpointMapperType extends EndpointMapper<EndpointType> | DefaultMapperType = DefaultMapperType, QueryParamsMapperType extends QueryParamsMapper<QueryParams> | DefaultMapperType = DefaultMapperType, HeaderMapperType extends HeaderMappingType | DefaultMapperType = DefaultMapperType, PayloadMapperType extends AdapterPayloadMappingType | DefaultMapperType = DefaultMapperType, > { /** Fetching function */ public unstable_fetcher: AdapterFetcherType< Adapter< AdapterOptions, MethodType, StatusType, Extra, QueryParams, DefaultQueryParams, EndpointType, EndpointMapperType, QueryParamsMapperType, HeaderMapperType, PayloadMapperType > >; /** * ******************** * Defaults * ******************** */ public name: string; public defaultMethod: MethodType; public defaultExtra: Extra; public systemErrorStatus: StatusType; public systemErrorExtra: Extra; public defaultRequestOptions?: RequestOptionsType<EndpointType, AdapterOptions, MethodType>; public logger: LoggerMethods; public initialized = false; public client: ClientInstance; public unstable_onInitializeCallback?: (options: { client: ClientInstance }) => void; public unstable_queryParamsMapperConfig: Parameters<QueryParamsMapperType>[1]; public unstable_headerMapperConfig: Parameters<HeaderMapperType>[1]; public unstable_payloadMapperConfig: Parameters<PayloadMapperType>[1]; public unstable_endpointMapperConfig: Parameters<EndpointMapperType>[1]; constructor( public options: { name: string; defaultMethod: MethodType; defaultExtra: Extra; systemErrorStatus: StatusType; systemErrorExtra: Extra; defaultRequestOptions?: RequestOptionsType<EndpointType, AdapterOptions, MethodType>; }, ) { this.name = options.name; this.defaultMethod = options.defaultMethod; this.defaultExtra = options.defaultExtra; this.systemErrorStatus = options.systemErrorStatus; this.systemErrorExtra = options.systemErrorExtra; this.defaultRequestOptions = options.defaultRequestOptions; } /** Initialize the adapter with a client reference and set up logging. Called automatically when the client is created. */ initialize = (client: ClientInstance) => { this.logger = client.loggerManager.initialize(client, "Adapter"); this.initialized = true; this.client = client; this.unstable_onInitializeCallback?.({ client }); return this; }; /** Register a callback invoked once the adapter is initialized with a client. */ onInitialize = (callback: (options: { client: ClientInstance }) => void) => { this.unstable_onInitializeCallback = callback; return this; }; /** * ******************** * Options Setters * ******************** */ public unstable_internalErrorMapping: (error: ReturnType<typeof getErrorMessage>) => any = (error) => error; /** Method to get default headers and to map them based on the data format exchange, by default it handles FormData / JSON formats. */ public unstable_headerMapper: HeaderMapperType = getAdapterHeaders as HeaderMapperType; /** Method to get request data and transform them to the required format. It handles FormData and JSON by default. */ public unstable_payloadMapper: PayloadMapperType = getAdapterPayload as PayloadMapperType; /** Method to get the endpoint for the adapter request. */ public unstable_endpointMapper: EndpointMapperType = defaultMapper as EndpointMapperType; /** Method to get request data and transform them to the required format. */ public unstable_queryParamsMapper: QueryParamsMapperType = defaultMapper as QueryParamsMapperType; /** Get default adapter options for the request. */ public unstable_getAdapterDefaults?: ( request: ExtendRequest< RequestInstance, { client: Client< any, Adapter< AdapterOptions, MethodType, StatusType, Extra, QueryParams, DefaultQueryParams, EndpointType, EndpointMapperType, QueryParamsMapperType, HeaderMapperType, PayloadMapperType > >; } >, ) => AdapterOptions; /** Get default request options for the request. */ public unstable_getRequestDefaults?: ( options: RequestOptionsType<EndpointType, AdapterOptions, MethodType>, ) => Partial<RequestOptionsType<EndpointType, AdapterOptions, MethodType>>; /** * Get formatted endpoint name of the request. * Helpful in displaying long endpoints like in case of graphql schemas etc. */ public unstable_devtoolsEndpointGetter: (endpoint: string) => string = (endpoint) => endpoint; /** * ******************** * Methods * ******************** */ /** Set the default HTTP method used when a request does not specify one. */ setDefaultMethod = (method: MethodType) => { this.defaultMethod = method; return this; }; /** Set the default extra metadata attached to every request response (e.g., headers, status). */ setDefaultExtra = (extra: Extra) => { this.defaultExtra = extra; return this; }; /** Set a function that formats the endpoint string for display in devtools. */ setDevtoolsEndpointGetter = (callback: (endpoint: string) => string) => { this.unstable_devtoolsEndpointGetter = callback; return this; }; /** * This method allows to configure global defaults for the request configuration like method, auth, deduplication etc. */ setRequestDefaults = (callback: typeof this.unstable_getRequestDefaults): this => { this.unstable_getRequestDefaults = callback; return this; }; /** * Set the adapter default options added to every sent request */ setAdapterDefaults = ( callback: ( request: ExtendRequest< RequestInstance, { // No need for global error type, because we haven't sent the request yet client: Client< any, Adapter< AdapterOptions, MethodType, StatusType, Extra, QueryParams, DefaultQueryParams, EndpointType, EndpointMapperType, QueryParamsMapperType, HeaderMapperType, PayloadMapperType > >; } >, ) => AdapterOptions, ): this => { this.unstable_getAdapterDefaults = callback; return this; }; /** Set a custom mapping for internal adapter errors before they are returned in the response. */ setInternalErrorMapping = (callback: (error: ReturnType<typeof getErrorMessage>) => any) => { this.unstable_internalErrorMapping = callback; return this; }; /** * Set the custom header mapping function */ setHeaderMapper = <NewMapper extends HeaderMappingType>(headerMapper: NewMapper) => { this.unstable_headerMapper = ((req: Parameters<NewMapper>[0]) => headerMapper(req, this.unstable_headerMapperConfig as Parameters<NewMapper>[1])) as unknown as HeaderMapperType; return this as unknown as Adapter< AdapterOptions, MethodType, StatusType, Extra, QueryParams, DefaultQueryParams, EndpointType, EndpointMapperType, QueryParamsMapperType, NewMapper, PayloadMapperType >; }; /** * Set the request payload mapping function which gets triggered before request is send */ setPayloadMapper = <NewMapper extends AdapterPayloadMappingType>(payloadMapper: NewMapper) => { this.unstable_payloadMapper = ((req: Parameters<NewMapper>[0]) => payloadMapper( req, this.unstable_payloadMapperConfig as Parameters<NewMapper>[1], )) as unknown as PayloadMapperType; return this as unknown as Adapter< AdapterOptions, MethodType, StatusType, Extra, QueryParams, DefaultQueryParams, EndpointType, EndpointMapperType, QueryParamsMapperType, HeaderMapperType, NewMapper >; }; /** * Set the endpoint mapping function which transforms the endpoint string before the request is sent */ setEndpointMapper = <NewEndpointMapper extends EndpointMapper<EndpointType>>(endpointMapper: NewEndpointMapper) => { this.unstable_endpointMapper = ((endpoint: Parameters<NewEndpointMapper>[0]) => endpointMapper( endpoint, this.unstable_endpointMapperConfig as Parameters<NewEndpointMapper>[1], )) as unknown as EndpointMapperType; return this as unknown as Adapter< AdapterOptions, MethodType, StatusType, Extra, QueryParams, DefaultQueryParams, EndpointType, NewEndpointMapper, QueryParamsMapperType, HeaderMapperType, PayloadMapperType >; }; /** * Set the query params mapping function which get triggered before request get sent */ setQueryParamsMapper = <NewQueryParamsMapper extends QueryParamsMapper<QueryParams>>( queryParamsMapper: NewQueryParamsMapper, ) => { this.unstable_queryParamsMapper = ((queryParams: Parameters<NewQueryParamsMapper>[0]) => queryParamsMapper( queryParams, this.unstable_queryParamsMapperConfig as Parameters<NewQueryParamsMapper>[1], )) as unknown as QueryParamsMapperType; return this as unknown as Adapter< AdapterOptions, MethodType, StatusType, Extra, QueryParams, DefaultQueryParams, EndpointType, EndpointMapperType, NewQueryParamsMapper, HeaderMapperType, PayloadMapperType >; }; /** Set the configuration object passed as the second argument to the query params mapper. */ setQueryParamsMapperConfig = <NewQueryParamsMapperConfig extends Parameters<QueryParamsMapperType>[1]>( config: NewQueryParamsMapperConfig, ) => { this.unstable_queryParamsMapperConfig = config; return this; }; /** Set the configuration object passed as the second argument to the header mapper. */ setHeaderMapperConfig = <NewHeaderMapperConfig extends Parameters<HeaderMapperType>[1]>( config: NewHeaderMapperConfig, ) => { this.unstable_headerMapperConfig = config; return this; }; /** Set the configuration object passed as the second argument to the endpoint mapper. */ setEndpointMapperConfig = <NewEndpointMapperConfig extends Parameters<EndpointMapperType>[1]>( config: NewEndpointMapperConfig, ) => { this.unstable_endpointMapperConfig = config; return this; }; /** Set the configuration object passed as the second argument to the payload mapper. */ setPayloadMapperConfig = <NewPayloadMapperConfig extends Parameters<PayloadMapperType>[1]>( config: NewPayloadMapperConfig, ) => { this.unstable_payloadMapperConfig = config; return this; }; /** * ******************** * Fetching * ******************** */ /** Set the fetcher function that performs the actual network request. Must be called before any request can be executed. */ public setFetcher( fetcher: AdapterFetcherType< Adapter< AdapterOptions, MethodType, StatusType, Extra, QueryParams, DefaultQueryParams, EndpointType, EndpointMapperType, QueryParamsMapperType, HeaderMapperType, PayloadMapperType > >, ) { this.unstable_fetcher = fetcher.bind(this); return this; } /** Execute a request through the adapter pipeline: maps endpoint/headers/payload, runs plugins, then calls the fetcher. */ public async fetch( request: ExtendRequest< RequestInstance, { // No need for global error type, it doesn't matter to the fetcher itself client: Client< any, Adapter< AdapterOptions, MethodType, StatusType, Extra, QueryParams, DefaultQueryParams, EndpointType, EndpointMapperType, QueryParamsMapperType, HeaderMapperType, PayloadMapperType > >; } >, requestId: string, ): Promise<RequestResponseType<RequestInstance>> { let startTime: number | undefined; const execute = async (resolve: (value: ResponseType<any, any, any>) => void) => { try { if (!this.initialized) { throw new RequestProcessingError(`Adapter ${this.options.name} is not initialized`); } if (!this.unstable_fetcher) { throw new RequestProcessingError(`Fetcher for ${this.options.name} adapter is not set`); } this.client.triggerPlugins("onAdapterFetch", { adapter: this, request, requestId, }); const bindings = await getAdapterBindings< Adapter< AdapterOptions, MethodType, StatusType, Extra, QueryParams, DefaultQueryParams, EndpointType, EndpointMapperType, QueryParamsMapperType, HeaderMapperType, PayloadMapperType > >({ request, requestId, resolve, internalErrorMapping: this.unstable_internalErrorMapping, onStartTime: (time) => { startTime = time; }, }); if (request.unstable_mock && request.isMockerEnabled && request.client.isMockerEnabled) { return await mocker< Adapter< AdapterOptions, MethodType, StatusType, Extra, QueryParams, DefaultQueryParams, EndpointType, EndpointMapperType, QueryParamsMapperType, HeaderMapperType, PayloadMapperType > >(bindings); } return this.unstable_fetcher(bindings); } catch (error) { const onError = getAdapterOnError({ request, requestId, startTime: startTime || Date.now(), logger: this.logger, resolve, }); return onError({ error: this.unstable_internalErrorMapping(error as ReturnType<typeof getErrorMessage>), status: this.options.systemErrorStatus, extra: this.options.systemErrorExtra, }); } }; const promise = new Promise((resolve) => { execute(resolve); }); return promise as Promise<RequestResponseType<RequestInstance>>; } }