From a86f8edc8ba549266d92f5dc0f802a99eaaa1f92 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 1 Oct 2024 21:16:45 +0200 Subject: [PATCH 01/10] feat: Add Logger interface to the custom logger --- src/types/request-handler.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/types/request-handler.ts b/src/types/request-handler.ts index 51bbf50..a4df41d 100644 --- a/src/types/request-handler.ts +++ b/src/types/request-handler.ts @@ -368,10 +368,15 @@ export interface ExtendedRequestConfig< shouldStopPolling?: PollingFunction; } +export interface Logger { + warn(message?: any, ...optionalParams: any[]): void; + error?(message?: any, ...optionalParams: any[]): void; +} + export interface RequestHandlerConfig extends RequestConfig { fetcher?: FetcherInstance | null; - logger?: any; + logger?: Logger | null; } export type RequestConfig< From 198f6541096cd1ebd27d0b0764c84b3d3f16a309 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 1 Oct 2024 23:20:57 +0200 Subject: [PATCH 02/10] feat: 'fetcher' and 'logger' can be now used on per-request basis --- README.md | 38 ++++++++--------- src/request-handler.ts | 80 +++++++++++++++++++++--------------- src/types/api-handler.ts | 2 +- src/types/request-handler.ts | 33 ++++++++++++--- 4 files changed, 94 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index e200339..336bc33 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ You can also use all native `fetch()` settings. | withCredentials | `boolean` | `false` | Indicates whether credentials (such as cookies) should be included with the request. | | timeout | `number` | `30000` | You can set a request timeout for all requests or particular in milliseconds. | | dedupeTime | `number` | `1000` | Time window, in milliseconds, during which identical requests are deduplicated (treated as single request). | -| logger | `object` | `null` | You can additionally specify logger object with your custom logger to automatically log the errors to the console. It should contain at least `error` and `warn` functions. | +| logger | `Logger` | `null` | You can additionally specify logger object with your custom logger to automatically log the errors to the console. It should contain at least `error` and `warn` functions. | ## 🏷️ Headers @@ -291,25 +291,6 @@ You can also use all native `fetch()` settings. **Note:** Header keys are case-sensitive when specified in request objects. Ensure that the keys are provided in the correct case to avoid issues with header handling. -### How to Set Per-Request Headers - -To set headers for a specific request, include the `headers` option in the request configuration. This option accepts an `object` where the keys are the header names and the values are the corresponding header values. - -### Default Headers - -The `fetchff` plugin automatically injects a set of default headers into every request. These default headers help ensure that requests are consistent and include necessary information for the server to process them correctly. - -#### Default Headers Injected - -- **`Content-Type`**: `application/json;charset=utf-8` - Specifies that the request body contains JSON data and sets the character encoding to UTF-8. - -- **`Accept`**: `application/json, text/plain, */*` - Indicates the media types that the client is willing to receive from the server. This includes JSON, plain text, and any other types. - -- **`Accept-Encoding`**: `gzip, deflate, br` - Specifies the content encoding that the client can understand, including gzip, deflate, and Brotli compression. - ### Setting Headers Globally You can set default headers that will be included in all requests made with a specific `createApiFetcher` instance. This is useful for setting common headers like authentication tokens or content types. @@ -347,6 +328,19 @@ const { data } = await fetchf('https://api.example.com/endpoint', { }); ``` +### Default Headers + +The `fetchff` plugin automatically injects a set of default headers into every request. These default headers help ensure that requests are consistent and include necessary information for the server to process them correctly. + +- **`Content-Type`**: `application/json;charset=utf-8` + Specifies that the request body contains JSON data and sets the character encoding to UTF-8. + +- **`Accept`**: `application/json, text/plain, */*` + Indicates the media types that the client is willing to receive from the server. This includes JSON, plain text, and any other types. + +- **`Accept-Encoding`**: `gzip, deflate, br` + Specifies the content encoding that the client can understand, including gzip, deflate, and Brotli compression. + ## 🌀 Interceptors @@ -911,6 +905,10 @@ const api = createApiFetcher({ method: 'get', // Default request method. params: {}, // Default params added to all requests. data: {}, // Alias for 'body'. Default data passed to POST, PUT, DELETE and PATCH requests. + cacheTime: 300, // Cache is valid for 5 minutes + cacheKey: (config) => `${config.url}-${config.method}`, // Custom cache key based on URL and method + cacheBuster: (config) => config.method === 'POST', // Bust cache for POST requests + skipCache: (response, config) => response.status !== 200, // Skip caching on non-200 responses onError(error) { // Interceptor on error console.error('Request failed', error); diff --git a/src/request-handler.ts b/src/request-handler.ts index b92da7a..5703b33 100644 --- a/src/request-handler.ts +++ b/src/request-handler.ts @@ -10,6 +10,8 @@ import type { RequestHandlerReturnType, CreatedCustomFetcherInstance, FetcherConfig, + FetcherInstance, + Logger, } from './types/request-handler'; import type { BodyPayload, @@ -90,21 +92,6 @@ export function createRequestHandler( ...config, }; - /** - * Immediately create instance of custom fetcher if it is defined - */ - const customFetcher = handlerConfig.fetcher; - const requestInstance = customFetcher?.create(handlerConfig) || null; - - /** - * Get Provider Instance - * - * @returns {CreatedCustomFetcherInstance | null} Provider's instance - */ - const getInstance = (): CreatedCustomFetcherInstance | null => { - return requestInstance; - }; - /** * Gets a configuration value from `reqConfig`, defaulting to `handlerConfig` if not present. * @@ -121,14 +108,35 @@ export function createRequestHandler( : handlerConfig[name]; }; + /** + * Immediately create instance of custom fetcher if it is defined + */ + const customFetcher = getConfig(config, 'fetcher'); + const requestInstance = customFetcher?.create(handlerConfig) || null; + + /** + * Get Provider Instance + * + * @returns {CreatedCustomFetcherInstance | null} Provider's instance + */ + const getInstance = (): CreatedCustomFetcherInstance | null => { + return requestInstance; + }; + /** * Logs messages or errors using the configured logger's `warn` method. * + * @param {RequestConfig} reqConfig - Request config passed when making the request * @param {...(string | ResponseError)} args - Messages or errors to log. */ - const logger = (...args: (string | ResponseError)[]): void => { - if (handlerConfig.logger?.warn) { - handlerConfig.logger.warn(...args); + const logger = ( + reqConfig: RequestConfig, + ...args: (string | ResponseError)[] + ): void => { + const logger = getConfig(reqConfig, 'logger'); + + if (logger?.warn) { + logger.warn(...args); } }; @@ -136,30 +144,30 @@ export function createRequestHandler( * Build request configuration * * @param {string} url - Request url - * @param {RequestConfig} reqConfig - Request config passed when making the request + * @param {RequestConfig} requestConfig - Request config passed when making the request * @returns {RequestConfig} - Provider's instance */ const buildConfig = ( url: string, - reqConfig: RequestConfig, + requestConfig: RequestConfig, ): FetcherConfig => { const method = getConfig( - reqConfig, + requestConfig, 'method', ).toUpperCase() as Method; const isGetAlikeMethod = method === GET || method === HEAD; const dynamicUrl = replaceUrlPathParams( url, - getConfig(reqConfig, 'urlPathParams'), + getConfig(requestConfig, 'urlPathParams'), ); // The explicitly passed "params" - const explicitParams = getConfig(reqConfig, 'params'); + const explicitParams = getConfig(requestConfig, 'params'); // The explicitly passed "body" or "data" const explicitBodyData: BodyPayload = - getConfig(reqConfig, 'body') || getConfig(reqConfig, 'data'); + getConfig(requestConfig, 'body') || getConfig(requestConfig, 'data'); // Final body data let body: RequestConfig['data']; @@ -170,11 +178,14 @@ export function createRequestHandler( } // Native fetch compatible settings - const isWithCredentials = getConfig(reqConfig, 'withCredentials'); + const isWithCredentials = getConfig( + requestConfig, + 'withCredentials', + ); const credentials = isWithCredentials ? 'include' - : getConfig(reqConfig, 'credentials'); + : getConfig(requestConfig, 'credentials'); const urlPath = explicitParams ? appendQueryParams(dynamicUrl, explicitParams) @@ -182,8 +193,8 @@ export function createRequestHandler( const isFullUrl = urlPath.includes('://'); const baseURL = isFullUrl ? '' - : getConfig(reqConfig, 'baseURL') || - getConfig(reqConfig, 'apiUrl'); + : getConfig(requestConfig, 'baseURL') || + getConfig(requestConfig, 'apiUrl'); // Automatically stringify request body, if possible and when not dealing with strings if ( @@ -196,7 +207,7 @@ export function createRequestHandler( } return { - ...reqConfig, + ...requestConfig, credentials, body, method, @@ -217,7 +228,7 @@ export function createRequestHandler( requestConfig: RequestConfig, ): Promise => { if (!isRequestCancelled(error)) { - logger('API ERROR', error); + logger(requestConfig, 'API ERROR', error); } // Local interceptors @@ -417,7 +428,7 @@ export function createRequestHandler( // Restart the main retry loop pollingAttempt++; - logger(`Polling attempt ${pollingAttempt}...`); + logger(requestConfig, 'Polling attempt ' + pollingAttempt + '...'); await delayInvocation(pollingInterval); @@ -456,7 +467,10 @@ export function createRequestHandler( ); } - logger(`Attempt ${attempt + 1} failed. Retry in ${waitTime}ms.`); + logger( + mergedConfig, + `Attempt ${attempt + 1} failed. Retry in ${waitTime}ms.`, + ); await delayInvocation(waitTime); @@ -479,7 +493,7 @@ export function createRequestHandler( */ const outputResponse = ( response: FetchResponse | null, - requestConfig: RequestConfig, + requestConfig: RequestConfig, error: ResponseError | null = null, ): FetchResponse => { const defaultResponse = getConfig(requestConfig, 'defaultResponse'); diff --git a/src/types/api-handler.ts b/src/types/api-handler.ts index d786b6f..eeec9c5 100644 --- a/src/types/api-handler.ts +++ b/src/types/api-handler.ts @@ -106,7 +106,7 @@ export interface RequestEndpointFunction { * @example * interface EndpointsMethods { * getUser: Endpoint; - * getPosts: Endpoint; + * getPosts: Endpoint; * } */ export declare type Endpoint< diff --git a/src/types/request-handler.ts b/src/types/request-handler.ts index a4df41d..57ece97 100644 --- a/src/types/request-handler.ts +++ b/src/types/request-handler.ts @@ -366,6 +366,30 @@ export interface ExtendedRequestConfig< * @returns `true` to stop polling, `false` to continue. */ shouldStopPolling?: PollingFunction; + + /** + * A custom fetcher instance to handle requests instead of the default implementation. + * When `null`, the default fetch behavior is used. + * + * @example: + * const customFetcher: FetcherInstance = { create: () => ({ request: (config) => fetch(config.url) }) }; + * fetchf('/endpoint', { fetcher: customFetcher }); + * + * @default null + */ + fetcher?: FetcherInstance | null; + + /** + * A custom logger instance to handle warnings and errors. + * When `null`, logging is disabled. + * + * @example: + * const customLogger: Logger = { warn: console.warn, error: console.error }; + * fetchf('/endpoint', { logger: customLogger }); + * + * @default null (Logging is disabled) + */ + logger?: Logger | null; } export interface Logger { @@ -373,11 +397,10 @@ export interface Logger { error?(message?: any, ...optionalParams: any[]): void; } -export interface RequestHandlerConfig - extends RequestConfig { - fetcher?: FetcherInstance | null; - logger?: Logger | null; -} +export type RequestHandlerConfig< + ResponseData = any, + RequestBody = any, +> = RequestConfig; export type RequestConfig< ResponseData = any, From e5958f0f3655d2ddb9f9d4460cac60eae4d298cd Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Oct 2024 18:06:40 +0200 Subject: [PATCH 03/10] fix: Default Content Type was not applicable --- src/request-handler.ts | 22 +++++ test/request-handler.spec.ts | 179 +++++++++++++++-------------------- 2 files changed, 98 insertions(+), 103 deletions(-) diff --git a/src/request-handler.ts b/src/request-handler.ts index 5703b33..62623df 100644 --- a/src/request-handler.ts +++ b/src/request-handler.ts @@ -92,6 +92,17 @@ export function createRequestHandler( ...config, }; + if (config.retry) { + handlerConfig.retry = { ...defaultConfig.retry, ...config.retry }; + } + + if (config.headers) { + handlerConfig.headers = { + ...defaultConfig.headers, + ...config.headers, + }; + } + /** * Gets a configuration value from `reqConfig`, defaulting to `handlerConfig` if not present. * @@ -311,6 +322,17 @@ export function createRequestHandler( ..._reqConfig, } as RequestConfig; + if (_reqConfig.retry) { + mergedConfig.retry = { ...handlerConfig.retry, ..._reqConfig.retry }; + } + + if (_reqConfig.headers) { + mergedConfig.headers = { + ...handlerConfig.headers, + ..._reqConfig.headers, + }; + } + let response: FetchResponse | null = null; const fetcherConfig = buildConfig(url, mergedConfig); diff --git a/test/request-handler.spec.ts b/test/request-handler.spec.ts index ab55b18..b8c2541 100644 --- a/test/request-handler.spec.ts +++ b/test/request-handler.spec.ts @@ -71,41 +71,28 @@ describe('Request Handler', () => { requestHandler = createRequestHandler({}); }); - const buildConfig = (method: string, url: string, data: any, config: any) => - (requestHandler as any).buildConfig(url, { - ...config, - method, - data, - }); - it('should not differ when the same request is made', () => { - const result = buildConfig( - 'GET', - 'https://example.com/api', - { foo: 'bar' }, - { baseURL: 'abc' }, - ); + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'GET', + data: { foo: 'bar' }, + baseURL: 'abc', + }); - const result2 = buildConfig( - 'GET', - 'https://example.com/api', - { foo: 'bar' }, - { baseURL: 'abc' }, - ); + const result2 = requestHandler?.buildConfig('https://example.com/api', { + method: 'GET', + data: { foo: 'bar' }, + baseURL: 'abc', + }); expect(result).toEqual(result2); }); it('should handle GET requests correctly', () => { - const result = buildConfig( - 'GET', - 'https://example.com/api', - {}, - { - headers, - params: { foo: 'bar' }, - }, - ); + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'GET', + headers, + params: { foo: 'bar' }, + }); expect(result).toMatchObject({ url: 'https://example.com/api?foo=bar', @@ -115,14 +102,11 @@ describe('Request Handler', () => { }); it('should handle POST requests correctly', () => { - const result = buildConfig( - 'POST', - 'https://example.com/api', - { foo: 'bar' }, - { - headers, - }, - ); + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'POST', + data: { foo: 'bar' }, + headers, + }); expect(result).toMatchObject({ url: 'https://example.com/api', @@ -133,14 +117,11 @@ describe('Request Handler', () => { }); it('should handle PUT requests correctly', () => { - const result = buildConfig( - 'PUT', - 'https://example.com/api', - { foo: 'bar' }, - { - headers, - }, - ); + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'PUT', + data: { foo: 'bar' }, + headers, + }); expect(result).toMatchObject({ url: 'https://example.com/api', @@ -151,14 +132,11 @@ describe('Request Handler', () => { }); it('should handle DELETE requests correctly', () => { - const result = buildConfig( - 'DELETE', - 'https://example.com/api', - { foo: 'bar' }, - { - headers, - }, - ); + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'DELETE', + data: { foo: 'bar' }, + headers, + }); expect(result).toMatchObject({ url: 'https://example.com/api', @@ -169,20 +147,27 @@ describe('Request Handler', () => { }); it('should handle custom headers and config when both data and query params are passed', () => { - const result = buildConfig( - 'POST', - 'https://example.com/api', - { additional: 'info' }, - { - headers: { 'X-CustomHeader': 'Some token' }, - params: { foo: 'bar' }, - }, - ); + const mergedConfig = { + headers, + } as RequestConfig; + + mergedConfig.headers = { + ...mergedConfig.headers, + ...{ 'X-CustomHeader': 'Some token' }, + }; + + const result = requestHandler?.buildConfig('https://example.com/api', { + ...mergedConfig, + method: 'POST', + data: { additional: 'info' }, + params: { foo: 'bar' }, + }); expect(result).toMatchObject({ url: 'https://example.com/api?foo=bar', method: 'POST', headers: { + ...headers, 'X-CustomHeader': 'Some token', }, body: JSON.stringify({ additional: 'info' }), @@ -190,7 +175,10 @@ describe('Request Handler', () => { }); it('should handle empty data and config', () => { - const result = buildConfig('POST', 'https://example.com/api', null, {}); + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'POST', + data: null, + }); expect(result).toMatchObject({ url: 'https://example.com/api', @@ -200,12 +188,10 @@ describe('Request Handler', () => { }); it('should handle data as string', () => { - const result = buildConfig( - 'POST', - 'https://example.com/api', - 'rawData', - {}, - ); + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'POST', + data: 'rawData', + }); expect(result).toMatchObject({ url: 'https://example.com/api', @@ -215,14 +201,10 @@ describe('Request Handler', () => { }); it('should correctly append query params for GET-alike methods', () => { - const result = buildConfig( - 'head', - 'https://example.com/api', - {}, - { - params: { foo: [1, 2] }, - }, - ); + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'HEAD', + params: { foo: [1, 2] }, + }); expect(result).toMatchObject({ url: 'https://example.com/api?foo[]=1&foo[]=2', @@ -231,14 +213,11 @@ describe('Request Handler', () => { }); it('should handle POST with query params in config', () => { - const result = buildConfig( - 'POST', - 'https://example.com/api', - { additional: 'info' }, - { - params: { foo: 'bar' }, - }, - ); + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'POST', + data: { additional: 'info' }, + params: { foo: 'bar' }, + }); expect(result).toMatchObject({ url: 'https://example.com/api?foo=bar', @@ -248,7 +227,9 @@ describe('Request Handler', () => { }); it('should append credentials if flag is used', () => { - const result = buildConfig('POST', 'https://example.com/api', null, { + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'POST', + data: null, withCredentials: true, }); @@ -261,14 +242,10 @@ describe('Request Handler', () => { }); it('should not append query params to POST requests if body is set as data', () => { - const result = buildConfig( - 'POST', - 'https://example.com/api', - { - foo: 'bar', - }, - {}, - ); + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'POST', + data: { foo: 'bar' }, + }); expect(result).toMatchObject({ url: 'https://example.com/api', @@ -278,16 +255,12 @@ describe('Request Handler', () => { }); it('should not append body nor data to GET requests', () => { - const result = buildConfig( - 'GET', - 'https://example.com/api', - { foo: 'bar' }, - { - body: { additional: 'info' }, - data: { additional: 'info' }, - params: { foo: 'bar' }, - }, - ); + const result = requestHandler?.buildConfig('https://example.com/api', { + method: 'GET', + data: { foo: 'bar' }, + body: { additional: 'info' }, + params: { foo: 'bar' }, + }); expect(result).toMatchObject({ url: 'https://example.com/api?foo=bar', From ef4ce81c6b47dfe3eb8cc4fd4fb210d7769cacb9 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Oct 2024 21:20:50 +0200 Subject: [PATCH 04/10] refactor: Reduce output size by utilizing helper function --- src/request-handler.ts | 46 ++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/request-handler.ts b/src/request-handler.ts index 62623df..f61995c 100644 --- a/src/request-handler.ts +++ b/src/request-handler.ts @@ -92,16 +92,30 @@ export function createRequestHandler( ...config, }; - if (config.retry) { - handlerConfig.retry = { ...defaultConfig.retry, ...config.retry }; - } - - if (config.headers) { - handlerConfig.headers = { - ...defaultConfig.headers, - ...config.headers, - }; - } + /** + * Merges the specified property from the base configuration and the new configuration into the target configuration. + * + * @param {K} property - The property key to merge from the base and new configurations. Must be a key of RequestHandlerConfig. + * @param {RequestHandlerConfig} targetConfig - The configuration object that will receive the merged properties. + * @param {RequestHandlerConfig} baseConfig - The base configuration object that provides default values. + * @param {RequestHandlerConfig} newConfig - The new configuration object that contains user-specific settings to merge. + */ + const mergeConfig = ( + property: K, + targetConfig: RequestHandlerConfig, + baseConfig: RequestHandlerConfig, + newConfig: RequestHandlerConfig, + ) => { + if (newConfig[property]) { + targetConfig[property] = { + ...baseConfig[property], + ...newConfig[property], + }; + } + }; + + mergeConfig('retry', handlerConfig, defaultConfig, config); + mergeConfig('headers', handlerConfig, defaultConfig, config); /** * Gets a configuration value from `reqConfig`, defaulting to `handlerConfig` if not present. @@ -322,16 +336,8 @@ export function createRequestHandler( ..._reqConfig, } as RequestConfig; - if (_reqConfig.retry) { - mergedConfig.retry = { ...handlerConfig.retry, ..._reqConfig.retry }; - } - - if (_reqConfig.headers) { - mergedConfig.headers = { - ...handlerConfig.headers, - ..._reqConfig.headers, - }; - } + mergeConfig('retry', mergedConfig, handlerConfig, _reqConfig); + mergeConfig('headers', mergedConfig, handlerConfig, _reqConfig); let response: FetchResponse | null = null; const fetcherConfig = buildConfig(url, mergedConfig); From 152d502b14f501053473c170a020c07c49443a80 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Oct 2024 21:30:18 +0200 Subject: [PATCH 05/10] docs: Move fetcher function to Basic Settings --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 336bc33..df32aec 100644 --- a/README.md +++ b/README.md @@ -175,12 +175,11 @@ const { data, error } = await api.getUser({ Click to expand
-All the Request Settings can be used directly in the function or in the `endpoints` property (on per-endpoint basis). There are also two extra global settings for `createApiFetcher()`: +All the Request Settings can be directly used in the function as global settings for all endpoints. They can be also used within the `endpoints` property (on per-endpoint basis). The exposed `endpoints` property is as follows: -| Name | Type | Default | Description | -| --------- | ----------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| endpoints | `object` | | List of your endpoints. Each endpoint accepts all the settings below. They can be set globally, per-endpoint or per-request. | -| fetcher | `FetcherInstance` | | A custom adapter (an instance / object) that exposes `create()` function so to create instance of API Fetcher. The `create()` should return `request()` function that would be used when making the requests. The native `fetch()` is used if the fetcher is not provided. | +- **`endpoints`**: + Type: `EndpointsConfig` + List of your endpoints. Each endpoint is an object that accepts all the Request Settings (see the Basic Settings below). The endpoints are required to be specified. #### How It Works @@ -206,7 +205,7 @@ import { createApiFetcher } from 'fetchff'; const api = createApiFetcher({ apiUrl: 'https://example.com/api', endpoints: { - updateUserData: { + updateUser: { url: '/update-user/:id', method: 'POST', }, @@ -215,7 +214,7 @@ const api = createApiFetcher({ }); // Using api.request to make a POST request -const { data, error } = await api.request('updateUserData', { +const { data, error } = await api.request('updateUser', { body: { name: 'John Doe', // Data Payload }, @@ -280,6 +279,7 @@ You can also use all native `fetch()` settings. | timeout | `number` | `30000` | You can set a request timeout for all requests or particular in milliseconds. | | dedupeTime | `number` | `1000` | Time window, in milliseconds, during which identical requests are deduplicated (treated as single request). | | logger | `Logger` | `null` | You can additionally specify logger object with your custom logger to automatically log the errors to the console. It should contain at least `error` and `warn` functions. | +| fetcher | `FetcherInstance` | | A custom adapter (an instance / object) that exposes `create()` function so to create instance of API Fetcher. The `create()` should return `request()` function that would be used when making the requests. The native `fetch()` is used if the fetcher is not provided. | ## 🏷️ Headers From 2e12dcb0a6b46279c4b19518ef003d473e5b2418 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Oct 2024 21:35:50 +0200 Subject: [PATCH 06/10] chore: Add funding file --- FUNDING.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 FUNDING.md diff --git a/FUNDING.md b/FUNDING.md new file mode 100644 index 0000000..28efdb2 --- /dev/null +++ b/FUNDING.md @@ -0,0 +1,36 @@ +# Funding for fetchff + +fetchff is an open-source project aimed at simplifying and enhancing HTTP requests in JavaScript applications. As an open-source project, it thrives on community support and contributions. Your support helps ensure that fetchff remains sustainable and continues to grow. If you find fetchff useful and would like to support its ongoing development, here are a few ways you can help: + +## Ways to Support + +### 1. **GitHub Sponsorship** + +You can sponsor the fetchff project directly on GitHub. Your contributions help cover development costs and allow us to invest more time into improving the library. + +[This option is currently unavailable] + + + +### 2. **Patreon** + +Consider supporting fetchff through Patreon. Your monthly contributions will provide us with the resources needed to maintain and enhance the project. + +[Support Us on Patreon](https://www.patreon.com/mattccc) + +### 3. **Contributions** + +If financial support isn't feasible for you at the moment, there are other ways to contribute: + +- **Report Issues**: Help us identify bugs or areas for improvement by reporting issues on GitHub. +- **Feature Requests**: Share your ideas for new features or enhancements. +- **Documentation**: Contribute to our documentation to help others understand and use fetchff more effectively. +- **Spread the Word**: Share fetchff with your colleagues and on social media. The more users we have, the more vibrant our community becomes! + +## Thank You! + +Your support, whether financial or in the form of contributions, helps ensure the longevity and success of fetchff. Together, we can make this project sustainable and even better! + +--- + +For more information on how to contribute to fetchff, please refer to our [CONTRIBUTING.md](./CONTRIBUTING.md) file. From 19cb6bf46ab39eb1355a146044540746876e3f40 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Oct 2024 21:38:04 +0200 Subject: [PATCH 07/10] chore: Remove duplicated npmignore --- src/.npmignore | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/.npmignore diff --git a/src/.npmignore b/src/.npmignore deleted file mode 100644 index b703ea2..0000000 --- a/src/.npmignore +++ /dev/null @@ -1,18 +0,0 @@ -.git -.github -.gitignore -coverage -test -node_modules -package - -*.cjs.development.js -*.tgz -*.map -*.ts -!*.d.ts -tsconfig.json -.eslintrc.js - -.DS_Store -src \ No newline at end of file From afff16dd4f4df3cba0f91ef73954f5956e239b42 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Oct 2024 21:38:49 +0200 Subject: [PATCH 08/10] chore: Naming in the example --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index df32aec..21df8d2 100644 --- a/README.md +++ b/README.md @@ -1447,7 +1447,7 @@ fetchUserAndCreatePost(1, { title: 'New Post', content: 'This is a new post.' })
Click to expand
- You can implement a `useApi()` hook to handle the data fetching. Since this package has everything included, you don't really need anything more than a simple hook to utilize.

+ You can implement a `useFetcher()` hook to handle the data fetching. Since this package has everything included, you don't really need anything more than a simple hook to utilize.

Create `api.ts` file: @@ -1465,10 +1465,10 @@ export const api = createApiFetcher({ }); ``` -Create `useApi.ts` file: +Create `useFetcher.ts` file: ```tsx -export const useApi = (apiFunction) => { +export const useFetcher = (apiFunction) => { const [data, setData] = useState(null); const [error] = useState(null); const [isLoading, setLoading] = useState(true); @@ -1503,7 +1503,7 @@ export const ProfileComponent = ({ id }) => { data: profile, error, isLoading, - } = useApi(() => api.getProfile({ urlPathParams: { id } })); + } = useFetcher(() => api.getProfile({ urlPathParams: { id } })); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; From 7b4c67dec9bc807ebfc5e08751bf676b3a798882 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Oct 2024 21:41:05 +0200 Subject: [PATCH 09/10] docs: Improve comparison table --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 21df8d2..1dcb9c1 100644 --- a/README.md +++ b/README.md @@ -846,6 +846,7 @@ For a complete list of types and their definitions, refer to the [request-handle | **Unified API Client** | ✅ | -- | -- | -- | -- | | **Smart Request Cache** | ✅ | -- | -- | -- | -- | | **Automatic Request Deduplication** | ✅ | -- | -- | -- | -- | +| **Custom Fetching Adapter** | ✅ | -- | -- | -- | -- | | **Built-in Error Handling** | ✅ | -- | ✅ | -- | -- | | **Customizable Error Handling** | ✅ | -- | ✅ | ✅ | -- | | **Retries with exponential backoff** | ✅ | -- | -- | -- | -- | From 96168b3d313b27cf2d28d796e4fe7dafa3e60a31 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 2 Oct 2024 21:43:27 +0200 Subject: [PATCH 10/10] chore: Remove code of conduct from the npm ignore --- .npmignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.npmignore b/.npmignore index edf3213..145067c 100644 --- a/.npmignore +++ b/.npmignore @@ -7,7 +7,6 @@ node_modules package docs -CODE_OF_CONDUCT.md SECURITY.md *.tgz