{"id":21512606,"url":"https://github.com/mattccc/fetchff","last_synced_at":"2026-02-11T00:05:12.460Z","repository":{"id":42511363,"uuid":"338299612","full_name":"MattCCC/fetchff","owner":"MattCCC","description":"Fetchff is a lightweight, powerful and flexible HTTP client library designed to simplify request handling.","archived":false,"fork":false,"pushed_at":"2025-06-08T10:47:50.000Z","size":2531,"stargazers_count":37,"open_issues_count":8,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-08T11:28:50.410Z","etag":null,"topics":["ajax","api","api-handler","axios-api-handler","axios-instance","axios-multi-api","cache","fetch","fetchf","fetchff","http","http-client","http-request","javascript","nodejs","promise","promises","request","typescript","typescript-support"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/fetchff","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MattCCC.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":null,"patreon":"mattccc","open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2021-02-12T11:28:59.000Z","updated_at":"2025-06-04T17:39:36.000Z","dependencies_parsed_at":"2025-05-06T00:28:26.520Z","dependency_job_id":"00b6a7d6-6e46-4c99-b591-a86a742359e2","html_url":"https://github.com/MattCCC/fetchff","commit_stats":{"total_commits":130,"total_committers":4,"mean_commits":32.5,"dds":0.1384615384615384,"last_synced_commit":"e9cef24dc7d56207101838c5a8df5924ca46f110"},"previous_names":["mattccc/fetchf","mattccc/axios-multi-api"],"tags_count":61,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MattCCC%2Ffetchff","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MattCCC%2Ffetchff/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MattCCC%2Ffetchff/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MattCCC%2Ffetchff/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MattCCC","download_url":"https://codeload.github.com/MattCCC/fetchff/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MattCCC%2Ffetchff/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":258820799,"owners_count":22762988,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ajax","api","api-handler","axios-api-handler","axios-instance","axios-multi-api","cache","fetch","fetchf","fetchff","http","http-client","http-request","javascript","nodejs","promise","promises","request","typescript","typescript-support"],"created_at":"2024-11-23T22:41:05.482Z","updated_at":"2026-02-11T00:05:12.433Z","avatar_url":"https://github.com/MattCCC.png","language":"TypeScript","readme":"\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"./docs/logo.png\" alt=\"logo\" width=\"380\"/\u003e\n\n\u003ch4 align=\"center\"\u003eFast, lightweight (~5 KB gzipped) and reusable data fetching\u003c/h4\u003e\n\n\u003ci\u003e\"fetchff\" stands for \"fetch fast \u0026 flexibly\"\u003c/i\u003e\n\n[npm-url]: https://npmjs.org/package/fetchff\n[npm-image]: https://img.shields.io/npm/v/fetchff.svg\n\n[![NPM version][npm-image]][npm-url] [![Blazing Fast](https://badgen.now.sh/badge/speed/blazing%20%F0%9F%94%A5/green)](https://github.com/MattCCC/fetchff) [![Code Coverage](https://img.shields.io/badge/coverage-96.93-green)](https://github.com/MattCCC/fetchff) [![npm downloads](https://img.shields.io/npm/dm/fetchff.svg?color=lightblue)](http://npm-stat.com/charts.html?package=fetchff) [![gzip size](https://img.shields.io/bundlephobia/minzip/fetchff)](https://bundlephobia.com/result?p=fetchff) [![snyk](https://snyk.io/test/github/MattCCC/fetchff/badge.svg)](https://security.snyk.io/package/npm/fetchff)\n\n\u003c/div\u003e\n\n## Why?\n\nThis is a high level library to extend the functionality of native fetch() with everything necessary and no overhead, so to wrap and reuse common patterns and functionalities in a simple and declarative manner. It is designed to be used in high-throughput, high-performance applications.\n\nAlso, managing multitude of API connections in large applications can be complex, time-consuming and hard to scale. `fetchff` simplifies the process by offering a simple, declarative approach to API handling using Repository Pattern. It reduces the need for extensive setup, middlewares, retries, custom caching, and heavy plugins, and lets developers focus on data handling and application logic.\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n**Some of challenges with Native Fetch that `fetchff` solves:**\n\n- **Error Status Handling:** Fetch does not throw errors for HTTP error statuses, making it difficult to distinguish between successful and failed requests based on status codes alone.\n- **Error Visibility:** Error responses with status codes like 404 or 500 are not automatically propagated as exceptions, which can lead to inconsistent error handling.\n- **No Built-in Retry Mechanism:** Native `fetch()` lacks built-in support for retrying requests. Developers need to implement custom retry logic to handle transient errors or intermittent failures, which can be cumbersome and error-prone.\n- **Network Errors Handling:** Native `fetch()` only rejects the Promise for network errors or failure to reach the server. Issues such as timeout errors or server unavailability do not trigger rejection by default, which can complicate error management.\n- **Limited Error Information:** The error information provided by native `fetch()` is minimal, often leaving out details such as the request headers, status codes, or response bodies. This can make debugging more difficult, as there's limited visibility into what went wrong.\n- **Lack of Interceptors:** Native `fetch()` does not provide a built-in mechanism for intercepting requests or responses. Developers need to manually manage request and response processing, which can lead to repetitive code and less maintainable solutions.\n- **No Built-in Caching:** Native `fetch()` does not natively support caching of requests and responses. Implementing caching strategies requires additional code and management, potentially leading to inconsistencies and performance issues.\n\nTo address these challenges, the `fetchf()` provides several enhancements:\n\n1. **Consistent Error Handling:**\n   - In JavaScript, the native `fetch()` function does not reject the Promise for HTTP error statuses such as 404 (Not Found) or 500 (Internal Server Error). Instead, `fetch()` resolves the Promise with a `Response` object, where the `ok` property indicates the success of the request. If the request encounters a network error or fails due to other issues (e.g., server downtime), `fetch()` will reject the Promise.\n   - The `fetchff` plugin aligns error handling with common practices and makes it easier to manage errors consistently by rejecting erroneous status codes.\n\n2. **Enhanced Retry Mechanism:**\n   - **Retry Configuration:** You can configure the number of retries, delay between retries, and exponential backoff for failed requests. This helps to handle transient errors effectively.\n   - **Custom Retry Logic:** The `shouldRetry` asynchronous function allows for custom retry logic based on the error from `response.error` and attempt count, providing flexibility to handle different types of failures.\n   - **Retry Conditions:** Errors are only retried based on configurable retry conditions, such as specific HTTP status codes or error types.\n\n3. **Improved Error Visibility:**\n   - **Error Wrapping:** The `createApiFetcher()` and `fetchf()` wrap errors in a custom `ResponseError` class, which provides detailed information about the request and response. This makes debugging easier and improves visibility into what went wrong.\n\n4. **Extended settings:**\n   - Check Settings table for more information about all settings.\n   \u003c/details\u003e\n\n## ✔️ Benefits\n\n✅ **Lightweight:** Minimal code footprint of ~4KB gzipped for managing extensive APIs.\n\n✅ **High-Performance**: Optimized for speed and efficiency, ensuring fast and reliable API interactions.\n\n✅ **Secure:** Secure by default rather than \"permissive by default\", with built-in sanitization mechanisms.\n\n✅ **Immutable:** Every request has its own instance.\n\n✅ **Isomorphic:** Compatible with Node.js, Deno and modern browsers.\n\n✅ **Type Safe:** Strongly typed and written in TypeScript.\n\n✅ **Scalable:** Easily scales from a few calls to complex API networks with thousands of APIs.\n\n✅ **Tested:** Battle tested in large projects, fully covered by unit tests.\n\n✅ **Customizable:** Fully compatible with a wide range configuration options, allowing for flexible and detailed request customization.\n\n✅ **Responsible Defaults:** All settings are opt-in.\n\n✅ **Framework Independent**: Pure JavaScript solution, compatible with any framework or library, both client and server side.\n\n✅ **Browser and Node.js 18+ Compatible:** Works flawlessly in both modern browsers and Node.js environments.\n\n✅ **Maintained:** Since 2021 publicly through Github.\n\n## ✔️ Features\n\n- **Smart Retry Mechanism**: Features exponential backoff for intelligent error handling and retry mechanisms.\n- **Request Deduplication**: Set the time during which requests are deduplicated (treated as same request).\n- **Cache Management**: Dynamically manage cache with configurable expiration, custom keys, and selective invalidation.\n- **Network Revalidation**: Automatically revalidate data on window focus and network reconnection for fresh data.\n- **Dynamic URLs Support**: Easily manage routes with dynamic parameters, such as `/user/:userId`.\n- **Error Handling**: Flexible error management at both global and individual request levels.\n- **Request Cancellation**: Utilizes `AbortController` to cancel previous requests automatically.\n- **Adaptive Timeouts**: Smart timeout adjustment based on connection speed for optimal user experience.\n- **Fetching Strategies**: Handle failed requests with various strategies - promise rejection, silent hang, soft fail, or default response.\n- **Requests Chaining**: Easily chain multiple requests using promises for complex API interactions.\n- **Native `fetch()` Support**: Utilizes the built-in `fetch()` API, providing a modern and native solution for making HTTP requests.\n- **Custom Interceptors**: Includes `onRequest`, `onResponse`, and `onError` interceptors for flexible request and response handling.\n\n## ✔️ Install\n\n[![NPM](https://nodei.co/npm/fetchff.png)](https://npmjs.org/package/fetchff)\n\nUsing NPM:\n\n```bash\nnpm install fetchff\n```\n\nUsing Pnpm:\n\n```bash\npnpm install fetchff\n```\n\nUsing Yarn:\n\n```bash\nyarn add fetchff\n```\n\n## ✔️ API\n\n### Standalone usage\n\n#### `fetchf(url, config)`\n\n_Alias: `fetchff(url, config)`_\n\nA simple function that wraps the native `fetch()` and adds extra features like retries and better error handling. Use `fetchf()` directly for quick, enhanced requests - no need to set up `createApiFetcher()`. It works independently and is easy to use in any codebase.\n\n#### Example\n\n```typescript\nimport { fetchf } from 'fetchff';\n\nconst { data, error } = await fetchf('/api/user-details', {\n  timeout: 5000,\n  cancellable: true,\n  retry: { retries: 3, delay: 2000 },\n  // Specify some other settings here... The fetch() settings work as well...\n});\n```\n\n### Global Configuration\n\n#### `getDefaultConfig()`\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nReturns the current global default configuration used for all requests. This is useful for inspecting or debugging the effective global settings.\n\n```typescript\nimport { getDefaultConfig } from 'fetchff';\n\n// Retrieve the current global default config\nconst config = getDefaultConfig();\nconsole.log('Current global fetchff config:', config);\n```\n\n\u003c/details\u003e\n\n#### `setDefaultConfig(customConfig)`\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nAllows you to globally override the default configuration for all requests. This is useful for setting application-wide defaults like timeouts, headers, or retry policies.\n\n```typescript\nimport { setDefaultConfig } from 'fetchff';\n\n// Set global defaults for all requests\nsetDefaultConfig({\n  timeout: 10000, // 10 seconds for all requests\n  headers: {\n    Authorization: 'Bearer your-token',\n  },\n  retry: {\n    retries: 2,\n    delay: 1500,\n  },\n});\n\n// All subsequent requests will use these defaults\nconst { data } = await fetchf('/api/data'); // Uses 10s timeout and retry config\n```\n\n\u003c/details\u003e\n\n### Instance with many API endpoints\n\n#### `createApiFetcher(config)`\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nIt is a powerful factory function for creating API fetchers with advanced features. It provides a convenient way to configure and manage multiple API endpoints using a declarative approach. This function offers integration with retry mechanisms, error handling improvements, and all the other settings. Unlike traditional methods, `createApiFetcher()` allows you to set up and use API endpoints efficiently with minimal boilerplate code.\n\n#### Example\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\n// Create some endpoints declaratively\nconst api = createApiFetcher({\n  baseURL: 'https://example.com/api',\n  endpoints: {\n    getUser: {\n      url: '/user-details/:id/',\n      method: 'GET',\n      // Each endpoint accepts all settings declaratively\n      retry: { retries: 3, delay: 2000 },\n      timeout: 5000,\n      cancellable: true,\n    },\n    // Define more endpoints as needed\n  },\n  // You can set all settings globally\n  strategy: 'softFail', // no try/catch required in case of errors\n});\n\n// Make a GET request to http://example.com/api/user-details/2/?rating[]=1\u0026rating[]=2\nconst { data, error } = await api.getUser({\n  params: { rating: [1, 2] }, //  Passed arrays, objects etc. will be parsed automatically\n  urlPathParams: { id: 2 }, // Replace :id with 2 in the URL\n});\n```\n\n#### Multiple API Specific Settings\n\nAll 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:\n\n- **`endpoints`**:\n  Type: `EndpointsConfig\u003cEndpointTypes\u003e`\n  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.\n\n#### How It Works\n\nThe `createApiFetcher()` automatically creates and returns API methods based on the `endpoints` object provided. It also exposes some extra methods and properties that are useful to handle global config, dynamically add and remove endpoints etc.\n\n#### `api.yourEndpoint(requestConfig)`\n\nWhere `yourEndpoint` is the name of your endpoint, the key from `endpoints` object passed to the `createApiFetcher()`.\n\n**`requestConfig`** (optional) `object` - To have more granular control over specific endpoints you can pass Request Config for particular endpoint. Check \u003cb\u003eBasic Settings\u003c/b\u003e below for more information.\n\nReturns: \u003cb\u003eResponse Object\u003c/b\u003e (see below).\n\n#### `api.request(endpointNameOrUrl, requestConfig)`\n\nThe `api.request()` helper function is a versatile method provided for making API requests with customizable configurations. It allows you to perform HTTP requests to any endpoint defined in your API setup and provides a straightforward way to handle queries, path parameters, and request configurations dynamically.\n\n##### Example\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\nconst api = createApiFetcher({\n  apiUrl: 'https://example.com/api',\n  endpoints: {\n    updateUser: {\n      url: '/update-user/:id',\n      method: 'POST',\n    },\n    // Define more endpoints as needed\n  },\n});\n\n// Using api.request to make a POST request\nconst { data, error } = await api.request('updateUser', {\n  body: {\n    name: 'John Doe', // Data Payload\n  },\n  urlPathParams: {\n    id: '123', // URL Path Param :id will be replaced with 123\n  },\n});\n\n// Using api.request to make a GET request to an external API\nconst { data, error } = await api.request('https://example.com/api/user', {\n  params: {\n    name: 'John Smith', // Query Params\n  },\n});\n```\n\n#### `api.config`\n\nYou can access `api.config` property directly to modify global headers and other settings on the fly. This is a property, not a function.\n\n#### `api.endpoints`\n\nYou can access `api.endpoints` property directly to modify the endpoints list. This can be useful if you want to append or remove global endpoints. This is a property, not a function.\n\n\u003c/details\u003e\n\n### Advanced Utilities\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n#### Cache Management\n\n##### `mutate(key, newData, settings)`\n\nProgrammatically update cached data without making a network request. Useful for optimistic updates or reflecting changes from other operations.\n\n**Parameters:**\n\n- `key` (string): The cache key to update\n- `newData` (any): The new data to store in cache\n- `settings` (object, optional): Configuration options\n  - `revalidate` (boolean): Whether to trigger background revalidation after update\n\n```typescript\nimport { mutate } from 'fetchff';\n\n// Update cache for a specific cache key\nawait mutate('/api/users', newUserData);\n\n// Update with options\nawait mutate('/api/users', updatedData, {\n  revalidate: true, // Trigger background revalidation\n});\n```\n\n##### `getCache(key)`\n\nDirectly retrieve cached data for a specific cache key. Useful for reading the current cached response without triggering a network request.\n\n**Parameters:**\n\n- `key` (string): The cache key to retrieve (equivalent to `cacheKey` from request config or `config.cacheKey` from response object)\n\n**Returns:** The cached response object, or `null` if not found\n\n```typescript\nimport { getCache } from 'fetchff';\n\n// Get cached data for a specific key assuming you set {cacheKey: ''/api/user-profile'} in config\nconst cachedResponse = getCache('/api/user-profile');\nif (cachedResponse) {\n  console.log('Cached user profile:', cachedResponse.data);\n}\n```\n\n##### `setCache(key, response, ttl, staleTime)`\n\nDirectly set cache data for a specific key. Unlike `mutate()`, this doesn't trigger revalidation by default. This is a low level function to directly set cache data based on particular key. If unsure, use the `mutate()` with `revalidate: false` instead.\n\n**Parameters:**\n\n- `key` (string): The cache key to set. It must match the cache key of the request.\n- `response` (any): The full response object to store in cache\n- `ttl` (number, optional): Time to live for the cache entry, in seconds. Determines how long the cached data remains valid before expiring. If not specified, the default `0` value will be used (discard cache immediately), if `-1` specified then the cache will be held until manually removed using `deleteCache(key)` function.\n- `staleTime` (number, optional): Duration, in seconds, for which cached data is considered \"fresh\" before it becomes eligible for background revalidation. If not specified, the default stale time applies.\n\n```typescript\nimport { setCache } from 'fetchff';\n\n// Set cache data with custom ttl and staleTime\nsetCache('/api/user-profile', userData, 600, 60); // Cache for 10 minutes, fresh for 1 minute\n\n// Set cache for specific endpoint infinitely\nsetCache('/api/user-settings', userSettings, -1);\n```\n\n##### `deleteCache(key)`\n\nRemove cached data for a specific cache key. Useful for cache invalidation when you know data is stale.\n\n**Parameters:**\n\n- `key` (string): The cache key to delete\n\n```typescript\nimport { deleteCache } from 'fetchff';\n\n// Delete specific cache entry\ndeleteCache('/api/user-profile');\n\n// Delete cache after user logout\nconst logout = () =\u003e {\n  deleteCache('/api/user/*'); // Delete all user-related cache\n};\n```\n\n#### Revalidation Management\n\n##### `revalidate(key, isStaleRevalidation)`\n\nManually trigger revalidation for a specific cache entry, forcing a fresh network request to update the cached data.\n\n**Parameters:**\n\n- `key` (string): The cache key to revalidate\n- `isStaleRevalidation` (boolean, optional): Whether this is a background revalidation that doesn't mark as in-flight\n\n```typescript\nimport { revalidate } from 'fetchff';\n\n// Revalidate specific cache entry\nawait revalidate('/api/user-profile');\n\n// Revalidate with custom cache key\nawait revalidate('custom-cache-key');\n\n// Background revalidation (doesn't mark as in-flight)\nawait revalidate('/api/user-profile', true);\n```\n\n##### `revalidateAll(type, isStaleRevalidation)`\n\nTrigger revalidation for all cache entries associated with a specific event type (focus or online).\n\n**Parameters:**\n\n- `type` (string): The revalidation event type ('focus' or 'online')\n- `isStaleRevalidation` (boolean, optional): Whether this is a background revalidation\n\n```typescript\nimport { revalidateAll } from 'fetchff';\n\n// Manually trigger focus revalidation for all relevant entries\nrevalidateAll('focus');\n\n// Manually trigger online revalidation for all relevant entries\nrevalidateAll('online');\n```\n\n##### `removeRevalidators(type)`\n\nClean up revalidation event listeners for a specific event type. Useful for preventing memory leaks when you no longer need automatic revalidation.\n\n**Parameters:**\n\n- `type` (string): The revalidation event type to remove ('focus' or 'online')\n\n```typescript\nimport { removeRevalidators } from 'fetchff';\n\n// Remove all focus revalidation listeners\nremoveRevalidators('focus');\n\n// Remove all online revalidation listeners\nremoveRevalidators('online');\n\n// Typically called during cleanup\n// e.g., in React useEffect cleanup or when unmounting components\n```\n\n#### Pub/Sub System\n\n##### `subscribe(key, callback)`\n\nSubscribe to cache updates and data changes. Receive notifications when specific cache entries are updated.\n\n**Parameters:**\n\n- `key` (string): The cache key to subscribe to\n- `callback` (function): Function called when cache is updated\n  - `response` (any): The full response object\n\n**Returns:** Function to unsubscribe from updates\n\n```typescript\nimport { subscribe } from 'fetchff';\n\n// Subscribe to cache changes for a specific key\nconst unsubscribe = subscribe('/api/user-data', (response) =\u003e {\n  console.log('Cache updated with response:', response);\n  console.log('Response data:', response.data);\n  console.log('Response status:', response.status);\n});\n\n// Clean up subscription when no longer needed\nunsubscribe();\n```\n\n#### Request Management\n\n##### `abortRequest(key, error)`\n\nProgrammatically abort in-flight requests for a specific cache key or URL pattern.\n\n**Parameters:**\n\n- `key` (string): The cache key or URL pattern to abort\n- `error` (Error, optional): Custom error to throw for aborted requests\n\n```typescript\nimport { abortRequest } from 'fetchff';\n\n// Abort specific request by cache key\nabortRequest('/api/slow-operation');\n\n// Useful for cleanup when component unmounts or route changes\nconst cleanup = () =\u003e {\n  abortRequest('/api/user-dashboard');\n};\n```\n\n#### Network Detection\n\n##### `isSlowConnection()`\n\nCheck if the user is on a slow network connection (2G/3G). Useful for adapting application behavior based on connection speed.\n\n**Parameters:** None\n\n**Returns:** Boolean indicating if connection is slow\n\n```typescript\nimport { isSlowConnection } from 'fetchff';\n\n// Check connection speed and adapt behavior\nif (isSlowConnection()) {\n  console.log('User is on a slow connection');\n  // Reduce image quality, disable auto-refresh, etc.\n}\n\n// Use in conditional logic\nconst shouldAutoRefresh = !isSlowConnection();\nconst imageQuality = isSlowConnection() ? 'low' : 'high';\n```\n\n\u003c/details\u003e\n\n## 🛠️ Plugin API Architecture\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n![Example SVG](./docs/api-architecture.png)\n\n\u003c/details\u003e\n\n## ⚙️ Basic Settings\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nYou can pass the settings:\n\n- globally for all requests when calling `createApiFetcher()`\n- per-endpoint basis defined under `endpoints` in global config when calling `createApiFetcher()`\n- per-request basis when calling `fetchf()` (second argument of the function) or in the `api.yourEndpoint()` (third argument)\n\nYou can also use all native [`fetch()` settings](https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters).\n\n|                            | Type                                                                                                   | Default           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| -------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| baseURL\u003cbr\u003e(alias: apiUrl) | `string`                                                                                               | `undefined`       | Your API base url.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |\n| url                        | `string`                                                                                               | `undefined`       | URL path e.g. /user-details/get                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |\n| method                     | `string`                                                                                               | `'GET'`           | Default request method e.g. GET, POST, DELETE, PUT etc. All methods are supported.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |\n| params                     | `object`\u003cbr\u003e`URLSearchParams`\u003cbr\u003e`NameValuePair[]`                                                     | `undefined`       | Query Parameters - a key-value pairs added to the URL to send extra information with a request. If you pass an object, it will be automatically converted. It works with nested objects, arrays and custom data structures similarly to what `jQuery` used to do in the past. If you use `createApiFetcher()` then it is the first argument of your `api.yourEndpoint()` function. You can still pass configuration in 3rd argument if want to.\u003cbr\u003e\u003cbr\u003eYou can pass key-value pairs where the values can be strings, numbers, or arrays. For example, if you pass `{ foo: [1, 2] }`, it will be automatically serialized into `foo[]=1\u0026foo[]=2` in the URL. |\n| body\u003cbr\u003e(alias: data)      | `object`\u003cbr\u003e`string`\u003cbr\u003e`FormData`\u003cbr\u003e`URLSearchParams`\u003cbr\u003e`Blob`\u003cbr\u003e`ArrayBuffer`\u003cbr\u003e`ReadableStream` | `undefined`       | The body is the data sent with the request, such as JSON, text, or form data, included in the request payload for POST, PUT, or PATCH requests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |\n| urlPathParams              | `object`                                                                                               | `undefined`       | It lets you dynamically replace segments of your URL with specific values in a clear and declarative manner. This feature is especially handy for constructing URLs with variable components or identifiers.\u003cbr\u003e\u003cbr\u003eFor example, suppose you need to update user details and have a URL template like `/user-details/update/:userId`. With `urlPathParams`, you can replace `:userId` with a real user ID, such as `123`, resulting in the URL `/user-details/update/123`.                                                                                                                                                                                  |\n| flattenResponse            | `boolean`                                                                                              | `false`           | When set to `true`, this option flattens the nested response data. This means you can access the data directly without having to use `response.data.data`. It works only if the response structure includes a single `data` property.                                                                                                                                                                                                                                                                                                                                                                                                                       |\n| select                     | `(data: any) =\u003e any`                                                                                   | `undefined`       | Function to transform or select a subset of the response data before it is returned. Called with the raw response data and should return the transformed data. Useful for mapping, picking, or shaping the response.                                                                                                                                                                                                                                                                                                                                                                                                                                        |\n| defaultResponse            | `any`                                                                                                  | `null`            | Default response when there is no data or when endpoint fails depending on the chosen `strategy`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |\n| withCredentials            | `boolean`                                                                                              | `false`           | Indicates whether credentials (such as cookies) should be included with the request. This equals to `credentials: \"include\"` in native `fetch()`. In Node.js, cookies are not managed automatically. Use a fetch polyfill or library that supports cookies if needed.                                                                                                                                                                                                                                                                                                                                                                                       |\n| timeout                    | `number`                                                                                               | `30000` / `60000` | You can set a request timeout in milliseconds. **Default is adaptive**: 30 seconds (30000 ms) for normal connections, 60 seconds (60000 ms) on slow connections (2G/3G). The timeout option applies to each individual request attempt including retries and polling. `0` means that the timeout is disabled.                                                                                                                                                                                                                                                                                                                                               |\n| dedupeTime                 | `number`                                                                                               | `0`               | Time window, in milliseconds, during which identical requests are deduplicated (treated as same request). If set to `0`, deduplication is disabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |\n| cacheTime                  | `number`                                                                                               | `undefined`       | Specifies the duration, in seconds, for which a cache entry is considered \"fresh.\" Once this time has passed, the entry is considered stale and may be refreshed with a new request. Set to -1 for indefinite cache. By default no caching.                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| staleTime                  | `number`                                                                                               | `undefined`       | Specifies the duration, in seconds, for which cached data is considered \"fresh.\" During this period, cached data will be returned immediately, but a background revalidation (network request) will be triggered to update the cache. If set to `0`, background revalidation is disabled and revalidation is triggered on every access.                                                                                                                                                                                                                                                                                                                     |\n| refetchOnFocus             | `boolean`                                                                                              | `false`           | When set to `true`, automatically revalidates (refetches) data when the browser window regains focus. **Note: This bypasses the cache and always makes a fresh network request** to ensure users see the most up-to-date data when they return to your application from another tab or window. Particularly useful for applications that display real-time or frequently changing data, but should be used judiciously to avoid unnecessary network traffic.                                                                                                                                                                                                |\n| refetchOnReconnect         | `boolean`                                                                                              | `false`           | When set to `true`, automatically revalidates (refetches) data when the browser regains internet connectivity after being offline. **This uses background revalidation to silently update data** without showing loading states to users. Helps ensure your application displays fresh data after network interruptions. Works by listening to the browser's `online` event.                                                                                                                                                                                                                                                                                |\n| 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.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| fetcher                    | `CustomFetcher`                                                                                        | `undefined`       | A custom fetcher async function. By default, the native `fetch()` is used. If you use your own fetcher, default response parsing e.g. `await response.json()` call will be skipped. Your fetcher should return response object / data directly.                                                                                                                                                                                                                                                                                                                                                                                                             |\n| parser                     | `(response: Response) =\u003e Promise\u003cany\u003e`                                                                 | `undefined`       | A custom response parser function. When provided, it replaces the built-in content-type based parsing entirely. Receives the raw `Response` object and should return the parsed data. Useful for handling custom formats like XML, CSV, or proprietary data. Can be set globally or per-request.                                                                                                                                                                                                                                                                                                                                                            |\n\n\u003e 📋 **Additional Settings Available:**  \n\u003e The table above shows the most commonly used settings. Many more advanced configuration options are available and documented in their respective sections below, including:\n\u003e\n\u003e - **🔄 Retry Mechanism** - `retries`, `delay`, `maxDelay`, `backoff`, `resetTimeout`, `retryOn`, `shouldRetry`\n\u003e - **📶 Polling Configuration** - `pollingInterval`, `pollingDelay`, `maxPollingAttempts`, `shouldStopPolling`\n\u003e - **🗄️ Cache Management** - `cacheKey`, `cacheBuster`, `skipCache`, `cacheErrors`\n\u003e - **✋ Request Cancellation** - `cancellable`, `rejectCancelled`\n\u003e - **🌀 Interceptors** - `onRequest`, `onResponse`, `onError`, `onRetry`\n\u003e - **🔍 Error Handling** - `strategy`\n\n### Performance Implications of Settings\n\nUnderstanding the performance impact of different settings helps you optimize for your specific use case:\n\n#### **High-Performance Settings**\n\n**Minimize Network Requests:**\n\n```typescript\n// Aggressive caching for static data\nconst staticConfig = {\n  cacheTime: 3600, // 1 hour cache\n  staleTime: 1800, // 30 minutes freshness\n  dedupeTime: 10000, // 10 seconds deduplication\n};\n\n// Result: 90%+ reduction in network requests\n```\n\n**Optimize for Mobile/Slow Connections:**\n\n```typescript\nconst mobileOptimized = {\n  timeout: 60000, // Longer timeout for slow connections (auto-adaptive)\n  retry: {\n    retries: 5, // More retries for unreliable connections\n    delay: 2000, // Longer initial delay (auto-adaptive)\n    backoff: 2.0, // Aggressive backoff\n  },\n  cacheTime: 900, // Longer cache on mobile\n};\n```\n\n#### **Memory vs Network Trade-offs**\n\n**Memory-Efficient (Low Cache):**\n\n```typescript\nconst memoryEfficient = {\n  cacheTime: 60, // Short cache (1 minute)\n  staleTime: undefined, // No stale-while-revalidate\n  dedupeTime: 1000, // Short deduplication\n};\n// Pros: Low memory usage\n// Cons: More network requests, slower perceived performance\n```\n\n**Network-Efficient (High Cache):**\n\n```typescript\nconst networkEfficient = {\n  cacheTime: 1800, // Long cache (30 minutes)\n  staleTime: 300, // 5 minutes stale-while-revalidate\n  dedupeTime: 5000, // Longer deduplication\n};\n// Pros: Fewer network requests, faster user experience\n// Cons: Higher memory usage, potentially stale data\n```\n\n#### **Feature Performance Impact**\n\n| Feature                    | Performance Impact                  | Best Use Case                               |\n| -------------------------- | ----------------------------------- | ------------------------------------------- |\n| **Caching**                | ⬇️ 70-90% fewer requests            | Static or semi-static data                  |\n| **Deduplication**          | ⬇️ 50-80% fewer concurrent requests | High-traffic applications                   |\n| **Stale-while-revalidate** | ⬆️ 90% faster perceived loading     | Dynamic data that tolerates brief staleness |\n| **Request cancellation**   | ⬇️ Reduced bandwidth waste          | Search-as-you-type, rapid navigation        |\n| **Retry mechanism**        | ⬆️ 95%+ success rate                | Mission-critical operations                 |\n| **Polling**                | ⬆️ Real-time updates                | Live data monitoring                        |\n\n#### **Adaptive Performance by Connection**\n\nFetchFF automatically adapts timeouts and retry delays based on connection speed:\n\n```typescript\n// Automatic adaptation (no configuration needed)\nconst adaptiveRequest = fetchf('/api/data');\n\n// On fast connections (WiFi/4G):\n// - timeout: 30 seconds\n// - retry delay: 1 second → 1.5s → 2.25s...\n// - max retry delay: 30 seconds\n\n// On slow connections (2G/3G):\n// - timeout: 60 seconds\n// - retry delay: 2 seconds → 3s → 4.5s...\n// - max retry delay: 60 seconds\n```\n\n#### **Performance Patterns**\n\n**Progressive Loading (Best UX):**\n\n```typescript\n// Layer 1: Instant response with cache\nconst quickData = await fetchf('/api/summary', {\n  cacheTime: 300,\n  staleTime: 60,\n});\n\n// Layer 2: Background enhancement\nfetchf('/api/detailed-data', {\n  strategy: 'silent',\n  cacheTime: 600,\n  onResponse(response) {\n    updateUIWithDetailedData(response.data);\n  },\n});\n```\n\n**Bandwidth-Conscious Loading:**\n\n```typescript\n// Check connection before expensive operations\nimport { isSlowConnection } from 'fetchff';\n\nconst loadUserDashboard = async () =\u003e {\n  const isSlowConn = isSlowConnection();\n\n  // Essential data always loads\n  const userData = await fetchf('/api/user', {\n    cacheTime: isSlowConn ? 600 : 300, // Longer cache on slow connections\n  });\n\n  // Optional data only on fast connections\n  if (!isSlowConn) {\n    fetchf('/api/user/analytics', { strategy: 'silent' });\n    fetchf('/api/user/recommendations', { strategy: 'silent' });\n  }\n};\n```\n\n#### **Performance Monitoring**\n\nTrack key metrics to optimize your settings:\n\n```typescript\nconst performanceConfig = {\n  onRequest(config) {\n    console.time(`request-${config.url}`);\n  },\n\n  onResponse(response) {\n    console.timeEnd(`request-${response.config.url}`);\n\n    // Track cache hit rate\n    if (response.fromCache) {\n      incrementMetric('cache.hits');\n    } else {\n      incrementMetric('cache.misses');\n    }\n  },\n\n  onError(error) {\n    incrementMetric('requests.failed');\n    console.warn('Request failed:', error.config.url, error.status);\n  },\n};\n```\n\n\u003e ℹ️ **Note:** This is just an example. You need to implement the `incrementMetric` function yourself to record or report performance metrics as needed in your application.\n\n\u003c/details\u003e\n\n## 🏷️ Headers\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n`fetchff` provides robust support for handling HTTP headers in your requests. You can configure and manipulate headers at both global and per-request levels. Here’s a detailed overview of how to work with headers using `fetchff`.\n\n**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.\n\n### Setting Headers Globally\n\nYou 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.\n\n#### Example: Setting Headers Globally\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\nconst api = createApiFetcher({\n  baseURL: 'https://api.example.com/',\n  headers: {\n    'Content-Type': 'application/json',\n    Authorization: 'Bearer YOUR_TOKEN',\n  },\n  // other configurations\n});\n```\n\n### Setting Per-Request Headers\n\nIn addition to global default headers, you can also specify headers on a per-request basis. This allows you to override global headers or set specific headers for individual requests.\n\n#### Example: Setting Per-Request Headers\n\n```typescript\nimport { fetchf } from 'fetchff';\n\n// Example of making a GET request with custom headers\nconst { data } = await fetchf('https://api.example.com/endpoint', {\n  headers: {\n    Authorization: 'Bearer YOUR_ACCESS_TOKEN',\n    'Custom-Header': 'CustomValue',\n  },\n});\n```\n\n### Default Headers\n\nThe `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.\n\n- **`Accept`**: `application/json, text/plain, */*`\n  Indicates the media types that the client is willing to receive from the server. This includes JSON, plain text, and any other types.\n\n- **`Accept-Encoding`**: `gzip, deflate, br`\n  Specifies the content encoding that the client can understand, including gzip, deflate, and Brotli compression.\n\n\u003e ⚠️ **Accept-Encoding in Node.js:**  \n\u003e In Node.js, decompression is handled by the fetch implementation, and users should ensure their environment supports the encodings.\n\n- **`Content-Type`**:  \n  Set automatically based on the request body type:\n  - For JSON-serializable bodies (objects, arrays, etc.):  \n    `application/json; charset=utf-8`\n  - For `URLSearchParams`:  \n    `application/x-www-form-urlencoded`\n  - For `ArrayBuffer`/typed arrays:  \n    `application/octet-stream`\n  - For `FormData`, `Blob`, `File`, or `ReadableStream`:  \n    **Not set** as the header is handled automatically by the browser and by Node.js 18+ native fetch.\n\n  The `Content-Type` header is **never overridden** if you set it manually.\n\n**Summary:**  \nYou only need to set headers manually if you want to override these defaults. Otherwise, `fetchff` will handle the correct headers for most use cases, including advanced scenarios like file uploads, form submissions, and binary data.\n\n\u003c/details\u003e\n\n## 🌀 Interceptors\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n  Interceptor functions can be provided to customize the behavior of requests and responses. These functions are invoked at different stages of the request lifecycle and allow for flexible handling of requests, responses, and errors.\n\n### Example\n\n```typescript\nconst { data } = await fetchf('https://api.example.com/', {\n  onRequest(config) {\n    // Add a custom header before sending the request\n    config.headers['Authorization'] = 'Bearer your-token';\n  },\n  onResponse(response) {\n    // Log the response status\n    console.log(`Response Status: ${response.status}`);\n  },\n  onError(error, config) {\n    // Handle errors and log the request config\n    console.error('Request failed:', error);\n    console.error('Request config:', config);\n  },\n  onRetry(response, attempt) {\n    // Log retry attempts for monitoring and debugging\n    console.warn(\n      `Retrying request (attempt ${attempt + 1}):`,\n      response.config.url,\n    );\n\n    // Modify config for the upcoming retry request\n    response.config.headers['Authorization'] = 'Bearer your-new-token';\n\n    // Log error details for failed attempts\n    if (response.error) {\n      console.warn(\n        `Retry reason: ${response.error.status} - ${response.error.statusText}`,\n      );\n    }\n\n    // You can implement custom retry logic or monitoring here\n    // For example, send retry metrics to your analytics service\n  },\n  retry: {\n    retries: 3,\n    delay: 1000,\n    backoff: 1.5,\n  },\n});\n```\n\n### Configuration\n\nThe following options are available for configuring interceptors in the `fetchff` settings:\n\n- **`onRequest(config) =\u003e config`**:  \n  Type: `RequestInterceptor | RequestInterceptor[]`  \n  A function or an array of functions that are invoked before sending a request. Each function receives the request configuration object as its argument, allowing you to modify request parameters, headers, or other settings.\n  _Default:_ `undefined` (no modification).\n\n- **`onResponse(response) =\u003e response`**:  \n  Type: `ResponseInterceptor | ResponseInterceptor[]`  \n  A function or an array of functions that are invoked when a response is received. Each function receives the full response object, enabling you to process the response, handle status codes, or parse data as needed.  \n  _Default:_ `undefined` (no modification).\n\n- **`onError(error) =\u003e error`**:  \n  Type: `ErrorInterceptor | ErrorInterceptor[]`  \n  A function or an array of functions that handle errors when a request fails. Each function receives the error and request configuration as arguments, allowing you to implement custom error handling logic or logging.  \n  _Default:_ `undefined` (no modification).\n\n- **`onRetry(response, attempt) =\u003e response`**:  \n  Type: `RetryInterceptor | RetryInterceptor[]`  \n  A function or an array of functions that are invoked before each retry attempt. Each function receives the response object (containing error information) and the current attempt number as arguments, allowing you to implement custom retry logging, monitoring, or conditional retry logic.  \n  _Default:_ `undefined` (no retry interception).\n\nAll interceptors are asynchronous and can modify the provided config or response objects. You don't have to return a value, but if you do, any returned properties will be merged into the original argument.\n\n### Interceptor Execution Order\n\n`fetchff` follows specific execution patterns for interceptor chains:\n\n#### **Request Interceptors: FIFO (First In, First Out)**\n\nRequest interceptors execute in the order they are defined - from global to specific:\n\n```typescript\n// Execution order: 1 → 2 → 3 → 4\nconst api = createApiFetcher({\n  onRequest: (config) =\u003e {\n    /* 1. Global interceptor */\n  },\n  endpoints: {\n    getData: {\n      onRequest: (config) =\u003e {\n        /* 2. Endpoint interceptor */\n      },\n    },\n  },\n});\n\nawait api.getData({\n  onRequest: (config) =\u003e {\n    /* 3. Request interceptor */\n  },\n});\n```\n\n#### **Response Interceptors: LIFO (Last In, First Out)**\n\nResponse interceptors execute in reverse order - from specific to global:\n\n```typescript\n// Execution order: 3 → 2 → 1\nconst api = createApiFetcher({\n  onResponse: (response) =\u003e {\n    /* 3. Global interceptor (executes last) */\n  },\n  endpoints: {\n    getData: {\n      onResponse: (response) =\u003e {\n        /* 2. Endpoint interceptor */\n      },\n    },\n  },\n});\n\nawait api.getData({\n  onResponse: (response) =\u003e {\n    /* 1. Request interceptor (executes first) */\n  },\n});\n```\n\nThis pattern ensures that:\n\n- **Request interceptors** can progressively enhance configuration from general to specific\n- **Response interceptors** can process data from specific to general, allowing request-level interceptors to handle the response first before global cleanup or logging\n\n### How It Works\n\n1. **Request Interception**:  \n   Before a request is sent, the `onRequest` interceptors are invoked. These interceptors can modify the request configuration, such as adding headers or changing request parameters.\n\n2. **Response Interception**:  \n   Once a response is received, the `onResponse` interceptors are called. These interceptors allow you to handle the response data, process status codes, or transform the response before it is returned to the caller.\n\n3. **Error Interception**:  \n   If a request fails and an error occurs, the `onError` interceptors are triggered. These interceptors provide a way to handle errors, such as logging or retrying requests, based on the error and the request configuration.\n\n4. **Custom Handling**:  \n   Each interceptor function provides a flexible way to customize request and response behavior. You can use these functions to integrate with other systems, handle specific cases, or modify requests and responses as needed.\n\n\u003c/details\u003e\n\n## 🌐 Network Revalidation\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n`fetchff` provides intelligent network revalidation features that automatically keep your data fresh based on user interactions and network connectivity. These features help ensure users always see up-to-date information without manual intervention.\n\n### Focus Revalidation\n\nWhen `refetchOnFocus` is enabled, requests are automatically triggered when the browser window regains focus (e.g., when users switch back to your tab).\n\n```typescript\nconst { data } = await fetchf('/api/user-profile', {\n  refetchOnFocus: true, // Revalidate when window gains focus\n  cacheTime: 300, // Cache for 5 minutes, but still revalidate on focus\n});\n```\n\n### Network Reconnection Revalidation\n\nThe `refetchOnReconnect` feature automatically revalidates data when the browser detects that internet connectivity has been restored after being offline.\n\n```typescript\nconst { data } = await fetchf('/api/notifications', {\n  refetchOnReconnect: true, // Revalidate when network reconnects\n  cacheTime: 600, // Cache for 10 minutes, but revalidate when back online\n});\n```\n\n### Adaptive Timeouts\n\n`fetchff` automatically adjusts request timeouts based on connection speed to provide optimal user experience:\n\n```typescript\n// Automatically uses:\n// - 30 seconds timeout on normal connections\n// - 60 seconds timeout on slow connections (2G/3G)\nconst { data } = await fetchf('/api/data');\n\n// You can still override with custom timeout\nconst { data: customTimeout } = await fetchf('/api/data', {\n  timeout: 10000, // Force 10 seconds regardless of connection speed\n});\n\n// Check connection speed manually\nimport { isSlowConnection } from 'fetchff';\n\nif (isSlowConnection()) {\n  console.log('User is on a slow connection');\n  // Adjust your app behavior accordingly\n}\n```\n\n### How It Works\n\n1. **Event Listeners**: `fetchff` automatically attaches global event listeners for `focus` and `online` events when needed\n2. **Background Revalidation**: Network revalidation uses background requests that don't show loading states to users\n3. **Automatic Cleanup**: Event listeners are properly managed and cleaned up to prevent memory leaks\n4. **Smart Caching**: Revalidation works alongside caching - fresh data updates the cache for future requests\n5. **Stale-While-Revalidate**: Use `staleTime` to control when background revalidation happens automatically\n6. **Connection Awareness**: Automatically detects connection speed and adjusts timeouts for better reliability\n\n### Configuration Options\n\nBoth revalidation features can be configured globally or per-request, and work seamlessly with cache timing:\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\nconst api = createApiFetcher({\n  baseURL: 'https://api.example.com',\n  // You can set all settings globally\n  refetchOnFocus: true,\n  refetchOnReconnect: true,\n  cacheTime: 300, // Cache for 5 minutes\n  staleTime: 60, // Consider fresh for 1 minute, then background revalidate\n  endpoints: {\n    getCriticalData: {\n      url: '/critical-data',\n      // Override global settings for specific endpoints\n      refetchOnFocus: true,\n      refetchOnReconnect: true,\n      staleTime: 30, // More aggressive background revalidation for critical data\n    },\n    getStaticData: {\n      url: '/static-data',\n      // Disable revalidation for static data\n      refetchOnFocus: false,\n      refetchOnReconnect: false,\n      staleTime: 3600, // Background revalidate after 1 hour\n    },\n  },\n});\n```\n\n### Use Cases\n\n**Focus Revalidation** is ideal for:\n\n- Real-time dashboards and analytics\n- Social media feeds and chat applications\n- Financial data and trading platforms\n- Any data that changes frequently while users are away\n\n**Reconnection Revalidation** is perfect for:\n\n- Mobile applications with intermittent connectivity\n- Offline-capable applications\n- Critical data that must be current when online\n- Applications used in areas with unstable internet\n\n### Best Practices\n\n1. **Combine with appropriate cache and stale times**:\n\n   ```typescript\n   const { data: notifications } = await fetchf('/api/notifications', {\n     cacheTime: 600, // Cache for 10 minutes\n     staleTime: 60, // Background revalidate after 1 minute\n     refetchOnFocus: true,\n   });\n\n   const { data: userProfile } = await fetchf('/api/profile', {\n     cacheTime: 1800, // Cache for 30 minutes\n     staleTime: 600, // Background revalidate after 10 minutes\n     refetchOnReconnect: true,\n   });\n   ```\n\n2. **Use `staleTime` for automatic background updates** - Data stays fresh without user interaction:\n\n   ```typescript\n   // Good: Automatic background revalidation for dynamic data\n   const { data: notifications } = await fetchf('/api/notifications', {\n     cacheTime: 600, // Cache for 10 minutes\n     staleTime: 60, // Background revalidate after 1 minute\n     refetchOnFocus: true,\n   });\n\n   // Good: Less frequent updates for semi-static data\n   const { data: userProfile } = await fetchf('/api/profile', {\n     cacheTime: 1800, // Cache for 30 minutes\n     staleTime: 600, // Background revalidate after 10 minutes\n     refetchOnReconnect: true,\n   });\n   ```\n\n3. **Use selectively** - Don't enable for all requests to avoid unnecessary network traffic:\n\n   ```typescript\n   // Good: Enable for critical, changing data\n   const { data: userNotifications } = await fetchf('/api/notifications', {\n     refetchOnFocus: true,\n     refetchOnReconnect: true,\n   });\n\n   // Avoid: Don't enable for static configuration data\n   const { data: appConfig } = await fetchf('/api/config', {\n     cacheTime: 3600, // Cache for 1 hour\n     staleTime: 0, // Disable background revalidation\n     refetchOnFocus: false,\n     refetchOnReconnect: false,\n   });\n   ```\n\n4. **Consider user experience** - Network revalidation happens silently in the background, providing smooth UX without loading spinners.\n\n\u003e ⚠️ **Browser Support**: These features work in all modern browsers that support the `focus` and `online` events. In server-side environments (Node.js), these options are safely ignored.\n\u003e\n\u003e **React Native**: Use `setEventProvider()` to enable these features. See the [React Native](#react-native) section for details.\n\n\u003c/details\u003e\n\n## 🗄️ Cache Management\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n  The caching mechanism in \u003cb\u003efetchf()\u003c/b\u003e and \u003cb\u003ecreateApiFetcher()\u003c/b\u003e enhances performance by reducing redundant network requests and reusing previously fetched data when appropriate. This system ensures that cached responses are managed efficiently and only used when considered \"fresh\". Below is a breakdown of the key parameters that control caching behavior and their default values.\n\u003cbr\u003e\u003cbr\u003e\n\n\u003e ⚠️ **When using in Node.js:**  \n\u003e Cache and deduplication are in-memory and per-process. For distributed or serverless environments, consider external caching if persistence is needed.\n\n### Example\n\n```typescript\nconst { data } = await fetchf('https://api.example.com/', {\n  cacheTime: 300, // Cache is valid for 5 minutes, set -1 for indefinite cache. By default no cache.\n  cacheKey: (config) =\u003e `${config.url}-${config.method}`, // Custom cache key based on URL and method, default automatically generated\n  cacheBuster: (config) =\u003e config.method === 'POST', // Bust cache for POST requests, by default no busting.\n  skipCache: (response, config) =\u003e response.status !== 200, // Skip caching on non-200 responses, by default no skipping\n  cacheErrors: false, // Cache error responses as well as successful ones, default false\n  staleTime: 600, // Data is considered fresh for 10 minutes before background revalidation (0 by default, meaning no background revalidation)\n});\n```\n\n### Configuration\n\nThe caching system can be fine-tuned using the following options when configuring the:\n\n- **`cacheTime`**:  \n  Type: `number`  \n  Specifies the duration, in seconds, for which a cache entry is considered \"fresh.\" Once this time has passed, the entry is considered stale and may be refreshed with a new request. Set to -1 for indefinite cache.\n  _Default:_ `undefined` (no caching).\n\n- **`cacheKey`**:  \n  Type: `CacheKeyFunction | string`  \n  A string or function used to generate a custom cache key for the request cache, deduplication etc. If not provided, a default key is created by hashing various parts of the request, including `Method`, `URL`, query parameters, and headers etc. Providing string can help to greatly improve the performance of the requests, avoid unnecessary request flooding etc.\n\n  You can provide either:\n  - A **string**: Used directly as the cache key for all requests using matching string.\n  - A **function**: Receives the full request config as an argument and should return a unique string key. This allows you to include any relevant part of the request (such as URL, method, params, body, or custom logic) in the cache key.\n\n  **Example:**\n\n  ```typescript\n  cacheKey: (config) =\u003e\n    `${config.method}:${config.url}:${JSON.stringify(config.params)}`;\n  ```\n\n  This flexibility ensures you can control cache granularity—whether you want to cache per endpoint, per user, or based on any other criteria.\n\n  _Default:_ Auto-generated based on request properties (see below).\n\n- **`cacheBuster`**:  \n  Type: `CacheBusterFunction`  \n  A function that allows you to invalidate or refresh the cache under certain conditions, such as specific request methods or response properties. This is useful for ensuring that certain requests (e.g., `POST`) bypass the cache.  \n  _Default:_ `(config) =\u003e false` (no cache busting).\n\n- **`skipCache`**:  \n  Type: `CacheSkipFunction`  \n  A function that determines whether caching should be skipped based on the response. This allows for fine-grained control over whether certain responses are cached or not, such as skipping non-`200` responses.  \n  _Default:_ `(response, config) =\u003e false` (no skipping).\n\n- **`cacheErrors`**:  \n  Type: `boolean`  \n  Determines whether error responses (such as HTTP 4xx or 5xx) should also be cached. If set to `true`, both successful and error responses are stored in the cache. If `false`, only successful responses are cached.  \n  _Default:_ `false`.\n\n- **`staleTime`**:  \n  Specifies the time in seconds during which cached data is considered \"fresh\" before it becomes stale and triggers background revalidation (SWR: stale-while-revalidate).\n  - Set to a number greater than `0` to enable SWR: cached data will be served instantly, and a background request will update the cache after this period.\n  - Set to `0` to treat data as stale immediately (always eligible for refetch).\n  - Set to `undefined` to disable SWR: data is never considered stale and background revalidation is not performed.  \n    _Default:_ `undefined` to disable SWR pattern (data is never considered stale) or `300` (5 minutes) in libraries like React.\n\n  ### How It Works\n  1. **Cache Lookup**:  \n     When a request is made, `fetchff` first checks the internal cache for a matching entry using the generated cache key. If a valid and \"fresh\" cache entry exists (within `cacheTime`), the cached response is returned immediately. If the native `fetch()` option `cache: 'reload'` is set, the internal cache is bypassed and a fresh request is made.\n\n  2. **Cache Key Generation**:  \n     Each request is uniquely identified by a cache key, which is auto-generated from the URL, method, params, headers, and other relevant options. You can override this by providing a custom `cacheKey` string or function for fine-grained cache control.\n\n  3. **Cache Busting**:  \n     If a `cacheBuster` function is provided, it determines whether to invalidate (bust) the cache for a given request. This is useful for scenarios like forcing fresh data on `POST` requests or after certain actions.\n\n  4. **Conditional Caching**:  \n     The `skipCache` function allows you to decide, per response, whether it should be stored in the cache. For example, you can skip caching for error responses (like HTTP 4xx/5xx) or based on custom logic.\n\n  5. **Network Request and Cache Update**:  \n     If no valid cache entry is found, or if caching is skipped or busted, the request is sent to the network. The response is then cached according to your configuration, making it available for future requests.\n\n### 🔄 Cache and Deduplication Integration\n\nUnderstanding how caching works together with request deduplication is crucial for optimal performance:\n\n#### **Cache-First, Then Deduplication**\n\n```typescript\n// Multiple components requesting the same data\nconst userProfile1 = useFetcher('/api/user/123', { cacheTime: 300 });\nconst userProfile2 = useFetcher('/api/user/123', { cacheTime: 300 });\nconst userProfile3 = useFetcher('/api/user/123', { cacheTime: 300 });\n\n// Flow:\n// 1. First request checks cache → cache miss → network request initiated\n// 2. Second request checks cache → cache miss → joins in-flight request (deduplication)\n// 3. Third request checks cache → cache miss → joins in-flight request (deduplication)\n// 4. When network response arrives → cache is populated → all requests receive same data\n```\n\n#### **Cache Hit Scenarios**\n\n```typescript\n// First request (cache miss - goes to network)\nconst request1 = fetchf('/api/data', { cacheTime: 300, dedupeTime: 5000 });\n\n// After 2 seconds - cache hit (no deduplication needed)\nsetTimeout(() =\u003e {\n  const request2 = fetchf('/api/data', { cacheTime: 300, dedupeTime: 5000 });\n  // Returns cached data immediately, no network request\n}, 2000);\n\n// After 10 minutes - cache expired, new request\nsetTimeout(() =\u003e {\n  const request3 = fetchf('/api/data', { cacheTime: 300, dedupeTime: 5000 });\n  // Cache expired → new network request → potential for deduplication again\n}, 600000);\n```\n\n#### **Deduplication Window vs Cache Time**\n\n- **`dedupeTime`**: Prevents duplicate requests during a short time window (milliseconds)\n- **`cacheTime`**: Stores successful responses for longer periods (seconds)\n- **Integration**: Deduplication handles concurrent requests, caching handles subsequent requests\n\n```typescript\nconst config = {\n  dedupeTime: 2000, // 2 seconds - for rapid concurrent requests\n  cacheTime: 300, // 5 minutes - for longer-term storage\n};\n\n// Timeline example:\n// T+0ms:   Request A initiated → network call starts\n// T+500ms: Request B initiated → joins Request A (deduplication)\n// T+1500ms: Request C initiated → joins Request A (deduplication)\n// T+2500ms: Request D initiated → deduplication window expired, but cache hit!\n// T+6000ms: Request E initiated → cache hit (no network call needed)\n```\n\n### ⏰ Understanding staleTime vs cacheTime\n\nThe relationship between `staleTime` and `cacheTime` enables sophisticated data freshness strategies:\n\n#### **Cache States and Timing**\n\n```typescript\nconst fetchWithTimings = fetchf('/api/user-feed', {\n  cacheTime: 600, // Cache for 10 minutes\n  staleTime: 60, // Consider fresh for 1 minute before background revalidation\n});\n\n// Data lifecycle:\n// T+0:     Fresh data - served from cache, no background request\n// T+30s:   Still fresh - served from cache, no background request\n// T+90s:   Stale but cached - served from cache + background revalidation\n// T+300s:  Still stale - served from cache + background revalidation\n// T+650s:  Cache expired - network request required, shows loading state\n```\n\n#### **Practical Combinations**\n\n**High-Frequency Updates (Real-time Data)**\n\n```typescript\nconst realtimeData = {\n  cacheTime: 30, // Cache for 30 seconds\n  staleTime: 5, // Fresh for 5 seconds only\n  // Result: Frequent background updates, always responsive UI\n};\n```\n\n**Balanced Performance (User Data)**\n\n```typescript\nconst userData = {\n  cacheTime: 300, // Cache for 5 minutes\n  staleTime: 60, // Fresh for 1 minute\n  // Result: Good performance + reasonable freshness\n};\n```\n\n**Static Content (Configuration)**\n\n```typescript\nconst staticConfig = {\n  cacheTime: 3600, // Cache for 1 hour\n  staleTime: 1800, // Fresh for 30 minutes\n  // Result: Minimal network usage for rarely changing data\n};\n```\n\n#### **Background Revalidation Behavior**\n\n```typescript\n// When staleTime expires but cacheTime hasn't:\nconst { data } = await fetchf('/api/notifications', {\n  cacheTime: 600, // 10 minutes total cache\n  staleTime: 120, // 2 minutes of \"freshness\"\n});\n\n// T+0:     Returns cached data immediately, no background request\n// T+150s:  Returns cached data immediately + triggers background request\n// T+150s:  Background request completes → cache silently updated\n// T+650s:  Cache expired → full loading state + network request\n```\n\n### Auto-Generated Cache Key Properties\n\nBy default, `fetchff` generates a cache key automatically using a combination of the following request properties:\n\n| Property          | Description                                                                               | Default Value   |\n| ----------------- | ----------------------------------------------------------------------------------------- | --------------- |\n| `method`          | The HTTP method used for the request (e.g., GET, POST).                                   | `'GET'`         |\n| `url`             | The full request URL, including the base URL and endpoint path.                           | `''`            |\n| `headers`         | Request headers, **filtered to include only cache-relevant headers** (see below).         |                 |\n| `body`            | The request payload (for POST, PUT, PATCH, etc.), stringified if it's an object or array. |                 |\n| `credentials`     | Indicates whether credentials (cookies) are included in the request.                      | `'same-origin'` |\n| `params`          | Query parameters serialized into the URL (objects, arrays, etc. are stringified).         |                 |\n| `urlPathParams`   | Dynamic URL path parameters (e.g., `/user/:id`), stringified and encoded.                 |                 |\n| `withCredentials` | Whether credentials (cookies) are included in the request.                                |                 |\n\n#### Header Filtering for Cache Keys\n\nTo ensure stable cache keys and prevent unnecessary cache misses, `fetchff` only includes headers that affect response content in cache key generation. The following headers are included:\n\n**Content Negotiation:**\n\n- `accept` - Affects response format (JSON, HTML, etc.)\n- `accept-language` - Affects localization of response\n- `accept-encoding` - Affects response compression\n\n**Authentication \u0026 Authorization:**\n\n- `authorization` - Affects access to protected resources\n- `x-api-key` - Token-based access control\n- `cookie` - Session-based authentication\n\n**Request Context:**\n\n- `content-type` - Affects how request body is interpreted\n- `origin` - Relevant for CORS or tenant-specific APIs\n- `referer` - May influence API behavior\n- `user-agent` - Only if server returns client-specific content\n\n**Custom Headers:**\n\n- `x-requested-with` - Distinguishes AJAX requests\n- `x-client-id` - Per-client/partner identity\n- `x-tenant-id` - Multi-tenant segmentation\n- `x-user-id` - Explicit user context\n- `x-app-version` - Version-specific behavior\n- `x-feature-flag` - Feature rollout controls\n- `x-device-id` - Device-specific responses\n- `x-platform` - Platform-specific content (iOS, Android, web)\n- `x-session-id` - Session-specific responses\n- `x-locale` - Locale-specific content\n\nHeaders like `user-agent`, `accept-encoding`, `connection`, `cache-control`, tracking IDs, and proxy-related headers are **excluded** from cache key generation as they don't affect the actual response content.\n\nThese properties are combined and hashed to create a unique cache key for each request. This ensures that requests with different parameters, bodies, or cache-relevant headers are cached separately while maintaining stable cache keys across requests that only differ in non-essential headers. If that does not suffice, you can always use `cacheKey` (string | function) and supply it to particular requests. You can also build your own `cacheKey` function and simply update defaults to reflect it in all requests. Auto key generation would be entirely skipped in such scenarios.\n\n\u003c/details\u003e\n\n## 🔁 Deduplication \u0026 In-Flight Requests\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n`fetchff` automatically deduplicates identical requests that are made within a configurable time window, ensuring that only one network request is sent for the same endpoint and parameters. This is especially useful for scenarios where multiple components or users might trigger the same request simultaneously (e.g., rapid user input, concurrent UI updates).\n\n\u003e ⚠️ **When using in Node.js:**  \n\u003e Request queueing and deduplication are per-process. In multi-process or serverless environments, requests are not deduplicated across instances.\n\n### How Deduplication Works\n\n- When a request is made, `fetchff` checks if an identical request (same URL, method, params, and body) is already in progress or was recently completed within the `dedupeTime` window.\n- If such a request exists, the new request will \"join\" the in-flight request and receive the same response when it completes, rather than triggering a new network call.\n- This mechanism reduces unnecessary network traffic and ensures that all consumers receive the same response for identical requests made in quick succession.\n\n### Configuration\n\n- **`dedupeTime`**:\n  - Type: `number`\n  - Default: `0` (milliseconds)\n  - Specifies the time window during which identical requests are deduplicated. If set to `0`, deduplication is disabled.\n\n### Example\n\n```typescript\nimport { fetchf } from 'fetchff';\n\n// Multiple rapid calls to the same endpoint will be deduplicated\nfetchf('/api/search', { params: { q: 'test' }, dedupeTime: 2000 });\nfetchf('/api/search', { params: { q: 'test' }, dedupeTime: 2000 });\n// Only one network request will be sent within the 2-second window\n```\n\n### Benefits\n\n- Prevents duplicate network requests for the same resource.\n- Reduces backend load and improves frontend performance.\n- Ensures that all consumers receive the same response for identical requests made in quick succession.\n\nThis deduplication logic is applied both to standalone `fetchf()` calls and to endpoints created with `createApiFetcher()`.\n\n\u003c/details\u003e\n\n## ✋ Request Cancellation\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\u003cb\u003efetchff\u003c/b\u003e simplifies making API requests by allowing customizable features such as request cancellation, retries, and response flattening. When a new request is made to the same API endpoint, the plugin automatically cancels any previous requests that haven't completed, ensuring that only the most recent request is processed.\n\u003cbr\u003e\u003cbr\u003e\nIt also supports:\n\n- Automatic retries for failed requests with configurable delay and exponential backoff.\n- Optional flattening of response data for easier access, removing nested `data` fields.\n\nYou can choose to reject cancelled requests or return a default response instead through the `defaultResponse` setting.\n\n### Example\n\n```javascript\nimport { fetchf } from 'fetchff';\n\n// Function to send the request\nconst sendRequest = () =\u003e {\n  // In this example, the previous requests are automatically cancelled\n  // You can also control \"dedupeTime\" setting in order to fire the requests more or less frequently\n  fetchf('https://example.com/api/messages/update', {\n    method: 'POST',\n    cancellable: true,\n    rejectCancelled: true,\n  });\n};\n\n// Attach keydown event listener to the input element with id \"message\"\ndocument.getElementById('message')?.addEventListener('keydown', sendRequest);\n```\n\n### Configuration\n\n- **`cancellable`**:\n  Type: `boolean`\n  Default: `false`\n  If set to `true`, any ongoing previous requests to the same API endpoint will be automatically cancelled when a subsequent request is made before the first one completes. This is useful in scenarios where repeated requests are made to the same endpoint (e.g., search inputs) and only the latest response is needed, avoiding unnecessary requests to the backend.\n\n- **`rejectCancelled`**:\n  Type: `boolean`\n  Default: `false`\n  Works in conjunction with the `cancellable` option. If set to `true`, the promise of a cancelled request will be rejected. By default (`false`), when a request is cancelled, instead of rejecting the promise, a `defaultResponse` will be returned, allowing graceful handling of cancellation without errors.\n\n\u003c/details\u003e\n\n## 📶 Polling Configuration\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n  Polling can be configured to repeatedly make requests at defined intervals until certain conditions are met. This allows for continuously checking the status of a resource or performing background updates.\n\n### Example\n\n```typescript\nconst { data } = await fetchf('https://api.example.com/', {\n  pollingInterval: 5000, // Poll every 5 seconds (useful for regular polling at intervals)\n  pollingDelay: 1000, // Wait 1 second before each polling attempt begins\n  maxPollingAttempts: 10, // Stop polling after 10 attempts\n  shouldStopPolling(response, attempt) {\n    if (response \u0026\u0026 response.status === 200) {\n      return true; // Stop polling if the response status is 200 (OK)\n    }\n    if (attempt \u003e= 10) {\n      return true; // Stop polling after 10 attempts\n    }\n    return false; // Continue polling otherwise\n  },\n});\n```\n\n### Configuration\n\nThe following options are available for configuring polling in the `RequestHandler`:\n\n- **`pollingInterval`**:  \n  Type: `number`  \n  Interval in milliseconds between polling attempts. If set to `0`, polling is disabled. This allows you to control the frequency of requests when polling is enabled. It is useful for regular, periodic polling.\n  _Default:_ `0` (polling disabled).\n\n- **`pollingDelay`**:  \n   Type: `number`  \n   The time (in milliseconds) to wait before each polling attempt begins. It is useful if you want to throttle or stagger requests, or wait a bit before each poll. It basically adds a delay before each poll is started (including the first one).\n  _Default:_ `0` (no delay).\n\n- **`maxPollingAttempts`**:  \n  Type: `number`  \n  Maximum number of polling attempts before stopping. Set to `0` or negative number for unlimited attempts.  \n  _Default:_ `0` (unlimited).\n\n- **`shouldStopPolling`**:  \n  Type: `(response: any, attempt: number) =\u003e boolean`  \n  A function to determine if polling should stop based on the response, error, or the current polling attempt number (attempt starts with `1`). Return `true` to stop polling, and `false` to continue polling. This allows for custom logic to decide when to stop polling based on the conditions of the response or error.  \n  _Default:_ `(response, attempt) =\u003e false` (polling continues indefinitely unless manually stopped).\n\n### How It Works\n\n1. **Polling Interval**:  \n   When `pollingInterval` is set to a non-zero value, polling begins after the initial request. The request is repeated at intervals defined by the `pollingInterval` setting.\n\n2. **Polling Delay**:  \n   The `pollingDelay` setting introduces a delay before each polling attempt, allowing for finer control over the timing of requests.\n\n3. **Maximum Polling Attempts**:  \n   The `maxPollingAttempts` setting limits the number of polling attempts. If the maximum number of attempts is reached, polling stops automatically.\n\n4. **Stopping Polling**:  \n   The `shouldStopPolling` function is invoked after each polling attempt. If it returns `true`, polling will stop. Otherwise, polling will continue until the condition to stop is met, or polling is manually stopped.\n\n5. **Custom Logic**:  \n   The `shouldStopPolling` function provides flexibility to implement custom logic based on the response, error, or the number of attempts. This makes it easy to stop polling when the desired outcome is reached or after a maximum number of attempts.\n\n\u003c/details\u003e\n\n## 🔄 Retry Mechanism\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\nThe retry mechanism can be used to handle transient errors and improve the reliability of network requests. This mechanism automatically retries requests when certain conditions are met, providing robustness in the face of temporary failures. Below is an overview of how the retry mechanism works and how it can be configured.\n\n### Example\n\n```typescript\nconst { data } = await fetchf('https://api.example.com/', {\n  retry: {\n    retries: 5,\n    delay: 100, // Override default adaptive delay (normally 1s/2s based on connection)\n    maxDelay: 5000, // Override default adaptive maxDelay (normally 30s/60s based on connection)\n    resetTimeout: true, // Resets the timeout for each retry attempt\n    backoff: 1.5,\n    retryOn: [500, 503],\n    // Retry on specific errors or based on custom logic\n    shouldRetry(response, attempt) {\n      // Retry if the status text is Not Found (404)\n      if (response.error \u0026\u0026 response.error.statusText === 'Not Found') {\n        return true;\n      }\n\n      // Use `response.data` to access any data from fetch() response\n      const data = response.data;\n\n      // Let's say your backend returns bookId as \"none\". You can force retry by returning \"true\".\n      if (data?.bookId === 'none') {\n        return true;\n      }\n\n      return attempt \u003c 3; // Retry up to 3 times.\n    },\n  },\n});\n```\n\nIn this example, the request will retry only on HTTP status codes 500 and 503, as specified in the `retryOn` array. The `resetTimeout` option ensures that the timeout is restarted for each retry attempt. The custom `shouldRetry` function adds further logic: if the server response contains `{\"bookId\": \"none\"}`, a retry is forced. Otherwise, the request will retry only if the current attempt number is less than 3. Although the `retries` option is set to 5, the `shouldRetry` function limits the maximum attempts to 3 (the initial request plus 2 retries).\n\n**Note:** When not overridden, `fetchff` automatically adapts retry delays based on connection speed:\n\n- **Normal connections**: 1s initial delay, 30s max delay\n- **Slow connections (2G/3G)**: 2s initial delay, 60s max delay\n\nAdditionally, you can handle \"Not Found\" (404) responses or other specific status codes in your retry logic. For example, you might want to retry when the status text is \"Not Found\":\n\n```typescript\nshouldRetry(response, attempt) {\n  // Retry if the status text is Not Found (404)\n  if (response.error \u0026\u0026 response.error.statusText === 'Not Found') {\n    return true;\n  }\n  // ...other logic\n\n  return null; // Fallback to `retryOn` status code check\n}\n```\n\nThis allows you to customize retry behavior for cases where a resource might become available after a short delay, or when you want to handle transient 404 errors gracefully.\n\nThe whole Error object is under `response.error` generally.\n\n### Configuration\n\nThe retry mechanism is configured via the `retry` option when instantiating the `RequestHandler`. You can customize the following parameters:\n\n- **`retries`**:  \n  Type: `number`  \n  Number of retry attempts to make after an initial failure.  \n  _Default:_ `0` (no retries).\n\n- **`delay`**:  \n  Type: `number`  \n  Initial delay (in milliseconds) before the first retry attempt. **Default is adaptive**: 1 second (1000 ms) for normal connections, 2 seconds (2000 ms) on slow connections (2G/3G). Subsequent retries use an exponentially increasing delay based on the `backoff` parameter.  \n  _Default:_ `1000` / `2000` (adaptive based on connection speed).\n\n- **`maxDelay`**:  \n  Type: `number`  \n  Maximum delay (in milliseconds) between retry attempts. **Default is adaptive**: 30 seconds (30000 ms) for normal connections, 60 seconds (60000 ms) on slow connections (2G/3G). The delay will not exceed this value, even if the exponential backoff would suggest a longer delay.  \n  _Default:_ `30000` / `60000` (adaptive based on connection speed).\n\n- **`backoff`**:  \n  Type: `number`  \n  Factor by which the delay is multiplied after each retry. For example, a `backoff` factor of `1.5` means each retry delay is 1.5 times the previous delay. It means that after the first failure, wait for x seconds. After the second failure, wait for x _ 1.5 seconds. After the third failure, wait for x _ 1.5^2 seconds, and so on.\n  _Default:_ `1.5`.\n\n- **`resetTimeout`**:  \n  Type: `boolean`  \n  If set to `true`, the timeout for the request is reset for each retry attempt. This ensures that the timeout applies to each individual retry rather than the entire request lifecycle.  \n  _Default:_ `true`.\n\n- **`retryOn`**:  \n  Type: `number[]`  \n  Array of HTTP status codes that should trigger a retry. By default, retries are triggered for the following status codes:\n  - `408` - Request Timeout\n  - `409` - Conflict\n  - `425` - Too Early\n  - `429` - Too Many Requests\n  - `500` - Internal Server Error\n  - `502` - Bad Gateway\n  - `503` - Service Unavailable\n  - `504` - Gateway Timeout\n\nIf used in conjunction with `shouldRetry`, the `shouldRetry` function takes priority, and falls back to `retryOn` only if it returns `null`.\n\n- **`shouldRetry(response: FetchResponse, currentAttempt: Number) =\u003e boolean`**:  \n  Type: `RetryFunction\u003cResponseData, RequestBody, QueryParams, PathParams\u003e`  \n  Function that determines whether a retry should be attempted \u003cb\u003ebased on the error\u003c/b\u003e or \u003cb\u003esuccessful response\u003c/b\u003e (if `shouldRetry` is provided) object, and the current attempt number. This function receives the error object and the attempt number as arguments. The boolean returned indicates decision. If `true` then it should retry, if `false` then abort and don't retry, if `null` then fallback to `retryOn` status codes check.\n  _Default:_ `undefined`.\n\n### How It Works\n\n1. **Initial Request**: When a request fails, the retry mechanism captures the failure and checks if it should retry based on the `retryOn` configuration and the result of the `shouldRetry` function.\n\n2. **Retry Attempts**: If a retry is warranted:\n   - The request is retried up to the specified number of attempts (`retries`).\n   - Each retry waits for a delay before making the next attempt. The delay starts at the initial `delay` value and increases exponentially based on the `backoff` factor, but will not exceed the `maxDelay`.\n   - If `resetTimeout` is enabled, the timeout is reset for each retry attempt.\n\n3. **Logging**: During retries, the mechanism logs warnings indicating the retry attempts and the delay before the next attempt, which helps in debugging and understanding the retry behavior.\n\n4. **Final Outcome**: If all retry attempts fail, the request will throw an error, and the final failure is processed according to the configured error handling logic.\n\n### 429 Retry-After Handling\n\nWhen a request receives a **429 Too Many Requests** response, `fetchff` will automatically check for the `Retry-After` header and use its value to determine the delay before the next retry attempt. This works for both seconds and HTTP-date formats, and falls back to your configured delay if the header is missing or invalid.\n\n**How it works:**\n\n- If the server responds with 429 and a `Retry-After` header, the delay for the next retry will be set to the value from that header (in ms).\n- If the header is missing or invalid, the default retry delay is used.\n\n**Example:**\n\n```typescript\nconst { data } = await fetchf('https://api.example.com/', {\n  retry: {\n    retries: 2,\n    delay: 1000, // fallback if Retry-After is missing\n    retryOn: [429], // 429 is already checked by default so it is not necessary to add it\n  },\n});\n```\n\nIf the server responds with:\n\n```\nHTTP/1.1 429 Too Many Requests\nRetry-After: 5\n```\n\nThe next retry will wait 5000ms before attempting again.\n\nIf the header is an HTTP-date, the delay will be calculated as the difference between the date and the current time.\n\n\u003c/details\u003e\n\n## 🧩 Response Data Transformation\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nThe `fetchff` plugin automatically handles response data transformation for any instance of `Response` returned by the `fetch()` (or a custom `fetcher`) based on the `Content-Type` header, ensuring that data is parsed correctly according to its format. It returns functions like `response.json()` that can be called idempotently without throwing errors or consuming the underlying stream multiple times. You can also use `parser` option to fully control returned response (useful for response streaming).\n\n### **How It Works**\n\n- **JSON (`application/json`):** Parses the response as JSON.\n- **Form Data (`multipart/form-data`):** Parses the response as `FormData`.\n- **Binary Data (`application/octet-stream`, images, video, audio, pdf, zip):** Parses the response as an `ArrayBuffer`. Use `response.blob()` to get a `Blob`, or `response.bytes()` to get a `Uint8Array`.\n- **URL-encoded Form Data (`application/x-www-form-urlencoded`):** Parses the response as `FormData`.\n- **Text (`text/*`):** Parses the response as plain text.\n- **Other/Custom types (XML, CSV, etc.):** Returns raw text. Use the `parser` option for custom parsing.\n\nIf the `Content-Type` header is missing or not recognized, the plugin defaults to returning text. If the text looks like JSON (starts with `{` or `[`), it will be auto-parsed as JSON.\n\nThis approach ensures that the `fetchff` plugin can handle a variety of response formats, providing a flexible and reliable method for processing data from API requests.\n\n\u003e ⚠️ **When using in Node.js:**  \n\u003e In Node.js, using FormData, Blob, or ReadableStream may require additional polyfills or will not work unless your fetch polyfill supports them.\n\n### Custom `parser` Option\n\nYou can provide a custom `parser` function to override the default content-type based parsing. This is useful for formats like XML, CSV, or any proprietary format. The `parser` can be set globally (via `createApiFetcher()` or `setDefaultConfig()`) or per-request.\n\n```typescript\nimport { fetchf } from 'fetchff';\n\n// Example: Parse XML responses\nconst { data } = await fetchf('/api/data.xml', {\n  async parser(response) {\n    const text = await response.text();\n    return new DOMParser().parseFromString(text, 'application/xml');\n  },\n});\n\n// Example: Parse CSV responses\nconst { data: csvData } = await fetchf('/api/report.csv', {\n  async parser(response) {\n    const text = await response.text();\n    return text.split('\\n').map((row) =\u003e row.split(','));\n  },\n});\n```\n\nYou can also set it globally:\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\nconst api = createApiFetcher({\n  apiUrl: 'https://example.com/api',\n  parser: async (response) =\u003e {\n    const text = await response.text();\n    return new DOMParser().parseFromString(text, 'application/xml');\n  },\n  endpoints: {\n    getReport: { url: '/report' },\n  },\n});\n```\n\n### `onResponse` Interceptor\n\nYou can use the `onResponse` interceptor to customize how the response is handled before it reaches your application. This interceptor gives you access to the raw `Response` object, allowing you to transform the data or modify the response behavior based on your needs.\n\n\u003c/details\u003e\n\n## 📄 Response Object\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n    \u003cbr\u003e\n  Every request returns a standardized response object from native \u003ccode\u003efetch()\u003c/code\u003e extended by a few handful properties:\n\n### Response Object Structure\n\n```typescript\ninterface FetchResponse\u003c\n  ResponseData = any,\n  RequestBody = any,\n  QueryParams = any,\n  PathParams = any,\n\u003e extends Response {\n  data: ResponseData | null; // The parsed response data, or null/defaultResponse if unavailable\n  error: ResponseError\u003c\n    ResponseData,\n    RequestBody,\n    QueryParams,\n    PathParams\n  \u003e | null; // Error details if the request failed, otherwise null\n  config: RequestConfig; // The configuration used for the request\n  status: number; // HTTP status code\n  statusText: string; // HTTP status text\n  headers: HeadersObject; // Response headers as a key-value object\n  isSuccess: boolean; // True if request is successful (2xx status codes).\n  isError: boolean; // True if the response contains an error\n}\n```\n\n- **`data`**:\n  The actual data returned from the API, or `null`/`defaultResponse` if not available.\n\n- **`error`**:\n  An object containing error details if the request failed, or `null` otherwise. Includes properties such as `name`, `message`, `status`, `statusText`, `request`, `config`, and the full `response`.\n\n- **`config`**:\n  The complete configuration object used for the request, including URL, method, headers, and parameters.\n\n- **`status`**:\n  The HTTP status code of the response (e.g., 200, 404, 500).\n\n- **`statusText`**:\n  The HTTP status text (e.g., 'OK', 'Not Found', 'Internal Server Error').\n\n- **`headers`**:\n  The response headers as a plain key-value object.\n\n- **`isSuccess`**:\n  Indicates whether the request was successful (2xx status codes).\n\n- **`isError`**:\n  True if the response is an error (non-2xx status code, network error, or request failed).\n\nThe whole response of the native `fetch()` is attached as well.\n\nError object in `error` looks as follows:\n\n- **Type**: `ResponseError\u003cResponseData, RequestBody, QueryParams, PathParams\u003e | null`\n\n- An object with details about any error that occurred or `null` otherwise.\n- **`name`**: The name of the error, that is `ResponseError`.\n- **`message`**: A descriptive message about the error.\n- **`status`**: The HTTP status code of the response (e.g., 404, 500).\n- **`statusText`**: The HTTP status text of the response (e.g., 'Not Found', 'Internal Server Error').\n- **`request`**: Details about the HTTP request that was sent (e.g., URL, method, headers).\n- **`config`**: The configuration object used for the request, including URL, method, headers, and query parameters.\n- **`response`**: The full response object received from the server, including all headers and body.\n- **`isCancelled`**: A boolean property on the error object indicating whether the request was cancelled before completion\n\n\u003c/details\u003e\n\n## 🔍 Error Handling\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n  Error handling strategies define how to manage errors that occur during requests. You can configure the \u003cb\u003estrategy\u003c/b\u003e option to specify what should happen when an error occurs. This affects whether promises are rejected, if errors are handled silently, or if default responses are provided. You can also combine it with \u003cb\u003eonError\u003c/b\u003e interceptor for more tailored approach.\n\n  \u003cbr\u003e\n  \u003cbr\u003e\n\nThe native `fetch()` API function doesn't throw exceptions for HTTP errors like `404` or `500` — it only rejects the promise if there is a network-level error (e.g. the request fails due to a DNS error, no internet connection, or CORS issues). The `fetchf()` function brings consistency and lets you align the behavior depending on chosen strategy. By default, all errors are rejected.\n\n### Configuration\n\n#### `strategy`\n\n**`reject`**: (default)\nPromises are rejected, and global error handling is triggered. You must use `try/catch` blocks to handle errors.\n\n```typescript\nimport { fetchf } from 'fetchff';\n\ntry {\n  const { data } = await fetchf('https://api.example.com/users', {\n    strategy: 'reject', // Default strategy - can be omitted\n    timeout: 5000,\n  });\n\n  console.log('Users fetched successfully:', data);\n} catch (error) {\n  // Handle specific error types\n  if (error.status === 404) {\n    console.error('API endpoint not found');\n  } else if (error.status \u003e= 500) {\n    console.error('Server error:', error.statusText);\n  } else {\n    console.error('Request failed:', error.message);\n  }\n}\n```\n\n**`softFail`**:  \n Returns a response object with additional property of `error` when an error occurs and does not throw any error. This approach helps you to handle error information directly within the response's `error` object without the need for `try/catch` blocks.\n\n\u003e ⚠️ **Always Check the error Property:**  \n\u003e When using the softFail or defaultResponse strategies, the promise will not throw on error.\n\u003e You must always check the error property in the response object to detect and handle errors.\n\n```typescript\nimport { fetchf } from 'fetchff';\n\nconst { data, error } = await fetchf('https://api.example.com/users', {\n  strategy: 'softFail',\n  timeout: 5000,\n});\n\nif (error) {\n  // Handle errors without try/catch\n  console.error('Request failed:', {\n    status: error.status,\n    message: error.message,\n    url: error.config?.url,\n  });\n\n  // Show user-friendly error message\n  if (error.status === 429) {\n    console.log('Rate limited. Please try again later.');\n  } else if (error.status \u003e= 500) {\n    console.log('Server temporarily unavailable. Please try again.');\n  }\n} else {\n  console.log('Users fetched successfully:', data);\n}\n```\n\nCheck `Response Object` section below to see how `error` object is structured.\n\n**`defaultResponse`**:  \n Returns a default response specified in case of an error. The promise will not be rejected. This can be used in conjunction with `flattenResponse` and `defaultResponse: {}` to provide sensible defaults.\n\n\u003e ⚠️ **Always Check the error Property:**  \n\u003e When using the softFail or defaultResponse strategies, the promise will not throw on error.\n\u003e You must always check the error property in the response object to detect and handle errors.\n\n```typescript\nimport { fetchf } from 'fetchff';\n\nconst { data, error } = await fetchf(\n  'https://api.example.com/user-preferences',\n  {\n    strategy: 'defaultResponse',\n    defaultResponse: {\n      theme: 'light',\n      language: 'en',\n      notifications: true,\n    },\n    timeout: 5000,\n  },\n);\n\nif (error) {\n  console.warn('Failed to load user preferences, using defaults:', data);\n  // Log error for debugging but continue with default values\n  console.error('Preferences API error:', error.message);\n} else {\n  console.log('User preferences loaded:', data);\n}\n\n// Safe to use data regardless of error state\ndocument.body.className = data.theme;\n```\n\n**`silent`**:  \n Hangs the promise silently on error, useful for fire-and-forget requests without the need for `try/catch`. In case of an error, the promise will never be resolved or rejected, and any code after will never be executed. This strategy is useful for dispatching requests within asynchronous wrapper functions that do not need to be awaited. It prevents excessive usage of `try/catch` or additional response data checks everywhere. It can be used in combination with `onError` to handle errors separately.\n\n\u003e ⚠️ **When using in Node.js:**  \n\u003e The 'silent' strategy will hang the promise forever. Use with caution, especially in backend/server environments.\n\n```typescript\nasync function myLoadingProcess() {\n  const { data } = await fetchf('https://api.example.com/', {\n    strategy: 'silent',\n  });\n\n  // In case of an error nothing below will ever be executed.\n  console.log('This console log will not appear.');\n}\n\nmyLoadingProcess();\n```\n\n##### How It Works\n\n1. **Reject Strategy**:  \n   When using the `reject` strategy, if an error occurs, the promise is rejected, and global error handling logic is triggered. You must use `try/catch` to handle these errors.\n\n2. **Soft Fail Strategy**:  \n   With `softFail`, the response object includes additional properties that provide details about the error without rejecting the promise. This allows you to handle error information directly within the response.\n\n3. **Default Response Strategy**:  \n   The `defaultResponse` strategy returns a predefined default response when an error occurs. This approach prevents the promise from being rejected, allowing for default values to be used in place of error data.\n\n4. **Silent Strategy**:  \n   The `silent` strategy results in the promise hanging silently on error. The promise will not be resolved or rejected, and any subsequent code will not execute. This is useful for fire-and-forget requests and can be combined with `onError` for separate error handling.\n\n5. **Custom Error Handling**:  \n   Depending on the strategy chosen, you can tailor how errors are managed, either by handling them directly within response objects, using default responses, or managing them silently.\n\n### 🎯 Choosing the Right Error Strategy\n\nUnderstanding when to use each error handling strategy is crucial for building robust applications:\n\n#### **`reject` Strategy - Traditional Error Handling**\n\n**When to Use:**\n\n- Building applications with established error boundaries\n- Need consistent error propagation through promise chains\n- Integration with existing try/catch error handling patterns\n- Critical operations where failures must be explicitly handled\n\n**Best For:**\n\n```typescript\n// API calls where failure must stop execution\ntry {\n  const { data } = await fetchf('/api/payment/process', {\n    method: 'POST',\n    body: paymentData,\n    strategy: 'reject', // Default - can be omitted\n  });\n\n  // Only proceed if payment succeeded\n  await processOrderCompletion(data);\n} catch (error) {\n  // Handle payment failure explicitly\n  showPaymentErrorModal(error.message);\n  revertOrderState();\n}\n```\n\n#### **`softFail` Strategy - Graceful Error Handling**\n\n**When to Use:**\n\n- Building user-friendly interfaces that degrade gracefully\n- Multiple API calls where some failures are acceptable\n- React/Vue components that need to handle loading/error states\n- Data fetching where partial failures shouldn't break the UI\n\n**Best For:**\n\n```typescript\n// Dashboard with multiple data sources\nconst { data: userStats, error: statsError } = await fetchf('/api/user/stats', {\n  strategy: 'softFail',\n});\nconst { data: notifications, error: notifError } = await fetchf('/api/notifications', {\n  strategy: 'softFail',\n});\n\n// Render what we can, gracefully handle what failed\nreturn (\n  \u003cDashboard\u003e\n    {userStats \u0026\u0026 \u003cStatsWidget data={userStats} /\u003e}\n    {statsError \u0026\u0026 \u003cErrorMessage\u003eStats temporarily unavailable\u003c/ErrorMessage\u003e}\n\n    {notifications \u0026\u0026 \u003cNotificationsList data={notifications} /\u003e}\n    {notifError \u0026\u0026 \u003cErrorMessage\u003eNotifications unavailable\u003c/ErrorMessage\u003e}\n  \u003c/Dashboard\u003e\n);\n```\n\n#### **`defaultResponse` Strategy - Fallback Values**\n\n**When to Use:**\n\n- Optional features that should work even when API fails\n- Configuration or preferences that have sensible defaults\n- Non-critical data that can fall back to static values\n- Progressive enhancement scenarios\n\n**Best For:**\n\n```typescript\n// User preferences with fallbacks\nconst { data: preferences } = await fetchf('/api/user/preferences', {\n  strategy: 'defaultResponse',\n  defaultResponse: {\n    theme: 'light',\n    language: 'en',\n    notifications: true,\n    autoSave: false,\n  },\n});\n\n// Safe to use preferences regardless of API status\napplyTheme(preferences.theme);\nsetLanguage(preferences.language);\n```\n\n#### **`silent` Strategy - Fire-and-Forget**\n\n**When to Use:**\n\n- Analytics and telemetry data\n- Non-critical background operations\n- Optional data prefetching\n- Logging and monitoring calls\n\n**Best For:**\n\n```typescript\n// Analytics tracking (don't let failures affect user experience)\nconst trackUserAction = (action: string, data: any) =\u003e {\n  fetchf('/api/analytics/track', {\n    method: 'POST',\n    body: { action, data, timestamp: Date.now() },\n    strategy: 'silent',\n    onError(error) {\n      // Log error for debugging, but don't disrupt user flow\n      console.warn('Analytics tracking failed:', error.message);\n    },\n  });\n\n  // This function never throws, never shows loading states\n  // User interaction continues uninterrupted\n};\n\n// Background data prefetching\nconst prefetchNextPage = () =\u003e {\n  fetchf('/api/articles/page/2', {\n    strategy: 'silent',\n    cacheTime: 300, // Cache for later use\n  });\n  // No need to await or handle response\n};\n```\n\n### 📊 Performance Strategy Matrix\n\nChoose strategies based on your application's needs:\n\n| Use Case                | Strategy          | Benefits                                          | Trade-offs                              |\n| ----------------------- | ----------------- | ------------------------------------------------- | --------------------------------------- |\n| **Critical Operations** | `reject`          | Explicit error handling, prevents data corruption | Requires try/catch, can break user flow |\n| **UI Components**       | `softFail`        | Graceful degradation, better UX                   | Need to check error property            |\n| **Optional Features**   | `defaultResponse` | Always provides usable data                       | May mask real issues                    |\n| **Background Tasks**    | `silent`          | Never disrupts user experience                    | Errors may go unnoticed                 |\n\n### 🔧 Advanced Strategy Patterns\n\n#### **Hybrid Error Handling**\n\n```typescript\n// Combine strategies for optimal UX\nconst fetchUserDashboard = async (userId: string) =\u003e {\n  // Critical user data - must succeed\n  const { data: userData } = await fetchf(`/api/users/${userId}`, {\n    strategy: 'reject',\n  });\n\n  // Optional widgets - graceful degradation\n  const { data: stats, error: statsError } = await fetchf(\n    `/api/users/${userId}/stats`,\n    {\n      strategy: 'softFail',\n    },\n  );\n\n  // Preferences with fallbacks\n  const { data: preferences } = await fetchf(\n    `/api/users/${userId}/preferences`,\n    {\n      strategy: 'defaultResponse',\n      defaultResponse: DEFAULT_USER_PREFERENCES,\n    },\n  );\n\n  // Background analytics - fire and forget\n  fetchf('/api/analytics/dashboard-view', {\n    method: 'POST',\n    body: { userId, timestamp: Date.now() },\n    strategy: 'silent',\n  });\n\n  return { userData, stats, statsError, preferences };\n};\n```\n\n#### **Progressive Enhancement**\n\n```typescript\n// Start with defaults, enhance with API data\nconst enhanceWithApiData = async () =\u003e {\n  // Immediate render with defaults\n  let config = DEFAULT_APP_CONFIG;\n  renderApp(config);\n\n  // Enhance with API data when available\n  const { data: apiConfig } = await fetchf('/api/config', {\n    strategy: 'defaultResponse',\n    defaultResponse: DEFAULT_APP_CONFIG,\n  });\n\n  // Re-render with enhanced config\n  config = { ...config, ...apiConfig };\n  renderApp(config);\n};\n```\n\n#### `onError`\n\nThe `onError` option can be configured to intercept errors:\n\n```typescript\nconst { data } = await fetchf('https://api.example.com/', {\n  strategy: 'softFail',\n  onError(error) {\n    // Intercept any error\n    console.error('Request failed', error.status, error.statusText);\n  },\n});\n```\n\n#### Different Error and Success Responses\n\nThere might be scenarios when your successful response data structure differs from the one that is on error. In such circumstances you can use union type and assign it depending on if it's an error or not.\n\n```typescript\ninterface SuccessResponseData {\n  bookId: string;\n  bookText: string;\n}\n\ninterface ErrorResponseData {\n  errorCode: number;\n  errorText: string;\n}\n\ntype ResponseData = SuccessResponseData | ErrorResponseData;\n\nconst { data, error } = await fetchf\u003cResponseData\u003e('https://api.example.com/', {\n  strategy: 'softFail',\n});\n\n// Check for error here as 'data' is available for both successful and erroneous responses\nif (error) {\n  const errorData = data as ErrorResponseData;\n\n  console.log('Request failed', errorData.errorCode, errorData.errorText);\n} else {\n  const successData = data as SuccessResponseData;\n\n  console.log('Request successful', successData.bookText);\n}\n```\n\n\u003c/details\u003e\n\n## 📦 Typings\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nThe `fetchff` package provides comprehensive TypeScript typings to enhance development experience and ensure type safety. Below are details on the available, exportable types for both `createApiFetcher()` and `fetchf()`.\n\n### Typings for `fetchf\u003cRequestType\u003e()`\n\nThe `fetchf()` function includes types that help configure and manage network requests effectively:\n\n```typescript\ninterface AddBookRequest {\n  response: AddBookResponseData;\n  params: AddBookQueryParams;\n  urlPathParams: AddBookPathParams;\n  body: AddBookRequestBody;\n}\n\n// You could also use: fetchf\u003cReq\u003cAddBookResponseData\u003e\u003e as a shorthand so not to create additional request interface\nconst { data: book } = await fetchf\u003cAddBookRequest\u003e('/api/add-book', {\n  method: 'POST',\n});\n// Your book is of type AddBookResponseData\n```\n\n- **`Req\u003cResponseData, RequestBody, QueryParams, UrlPathParams\u003e`**: Represents a shorter 4-generics version of request object type for endpoints, allowing you to compose the shape of the request payload, query parameters, and path parameters for each request using a couple inline generics e.g. `fetchf\u003cResponseData, RequestBody, QueryParams, UrlPathParams\u003e()`. While there is no plan for deprecation, this is for compatibility with older versions only. Aim to use the new method with single generic presented above instead. We don't use overload here to keep it all fast and snappy.\n- **`RequestConfig`**: Main configuration options for the `fetchf()` function, including request settings, interceptors, and retry configurations.\n- **`RetryConfig`**: Configuration options for retry mechanisms, including the number of retries, delay between retries, and backoff strategies.\n- **`CacheConfig`**: Configuration options for caching, including cache time, custom cache keys, and cache invalidation rules.\n- **`PollingConfig`**: Configuration options for polling, including polling intervals and conditions to stop polling.\n- **`ErrorStrategy`**: Defines strategies for handling errors, such as rejection, soft fail, default response, and silent modes.\n\nFor a complete list of types and their definitions, refer to the [request-handler.ts](https://github.com/MattCCC/fetchff/blob/master/src/types/request-handler.ts) file.\n\n### Typings for `createApiFetcher()`\n\nThe `createApiFetcher\u003cEndpointTypes\u003e()` function provides a robust set of types to define and manage API interactions.\n\n- **`EndpointTypes`**: Represents the list of API endpoints with their respective settings. It is your own interface that you can pass to this generic. It will be cross-checked against the `endpoints` object in your `createApiFetcher()` configuration. Each endpoint can be configured with its own specific types such as Response Data Structure, Query Parameters, URL Path Parameters or Request Body. Example:\n\n```typescript\ninterface EndpointTypes {\n  fetchBook: Endpoint\u003c{\n    response: Book;\n    params: BookQueryParams;\n    urlPathParams: BookPathParams;\n  }\u003e;\n  // or shorter version: fetchBook: EndpointReq\u003cBook, undefined, BookQueryParams, BookPathParams\u003e;\n  addBook: Endpoint\u003c{\n    response: Book;\n    body: BookBody;\n    params: BookQueryParams;\n    urlPathParams: BookPathParams;\n  }\u003e;\n  // or shorter version: fetchBook: EndpointReq\u003cBook, BookBody, BookQueryParams, BookPathParams\u003e;\n  someOtherEndpoint: Endpoint; // The generic is fully optional but it must be defined for endpoint not to output error\n}\n\nconst api = createApiFetcher\u003cEndpointTypes\u003e({\n  baseURL: 'https://example.com/api',\n  endpoints: {\n    fetchBook: {\n      url: '/get-book',\n    },\n    addBook: {\n      url: '/add-book',\n      method: 'POST',\n    },\n  },\n});\n\nconst { data: book } = await api.addBook();\n// book will be of type Book\n```\n\n\u003cbr\u003e\n\n- **`Endpoint\u003c{response: ResponseData, params: QueryParams, urlPathParams: PathParams, body: RequestBody}\u003e`**: Represents an API endpoint function, allowing to be defined with optional response data (default `DefaultResponse`), query parameters (default `QueryParams`), URL path parameters (default `DefaultUrlParams`), and request body (default `DefaultPayload`).\n- **`RequestInterceptor`**: Function to modify request configurations before they are sent.\n- **`ResponseInterceptor`**: Function to process responses before they are handled by the application.\n- **`ErrorInterceptor`**: Function to handle errors when a request fails.\n- **`CustomFetcher`**: Represents the custom `fetcher` function.\n\nFor a full list of types and detailed definitions, refer to the [api-handler.ts](https://github.com/MattCCC/fetchff/blob/master/src/types/api-handler.ts) file.\n\n### Generic Typings\n\nThe `fetchff` package includes several generic types to handle various aspects of API requests and responses:\n\n- **`QueryParams\u003cParamsType\u003e`**: Represents query parameters for requests. Can be an object, `URLSearchParams`, an array of name-value pairs, or `null`.\n- **`BodyPayload\u003cPayloadType\u003e`**: Represents the request body. Can be `BodyInit`, an object, an array, a string, or `null`.\n- **`UrlPathParams\u003cUrlParamsType\u003e`**: Represents URL path parameters. Can be an object or `null`.\n- **`DefaultResponse`**: Default response for all requests. Default is: `any`.\n\n### Benefits of Using Typings\n\n- **Type Safety**: Ensures that configurations and requests adhere to expected formats, reducing runtime errors and improving reliability.\n- **Autocompletion**: Provides better support for autocompletion in editors, making development faster and more intuitive.\n- **Documentation**: Helps in understanding available options and their expected values, improving code clarity and maintainability.\n\n\u003c/details\u003e\n\n## 🔒 Sanitization\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nFetchFF includes robust built-in sanitization mechanisms that protect your application from common security vulnerabilities. These safeguards are automatically applied without requiring any additional configuration.\n\n### Prototype Pollution Prevention\n\nThe library implements automatic protection against prototype pollution attacks by:\n\n- Removing dangerous properties like `__proto__`, `constructor`, and `prototype` from objects\n- Sanitizing all user-provided data before processing it\n\n```typescript\n// Example of protection against prototype pollution\nconst userInput = {\n  id: 123,\n  __proto__: { malicious: true },\n};\n\n// The sanitization happens automatically\nconst response = await fetchf('/api/users', {\n  params: userInput, // The __proto__ property will be removed\n});\n```\n\n### Input Sanitization Features\n\n1. **Object Sanitization**\n   - All incoming objects are sanitized via the `sanitizeObject` utility\n   - Creates shallow copies of input objects with dangerous properties removed\n   - Applied automatically to request configurations, headers, and other objects\n\n2. **URL Parameter Safety**\n   - Path parameters are properly encoded using `encodeURIComponent`\n   - Query parameters are safely serialized and encoded\n   - Prevents URL injection attacks and ensures valid URL formatting\n\n3. **Data Validation**\n   - Checks for JSON serializability of request bodies\n   - Detects circular references that could cause issues\n   - Properly handles different data types (strings, arrays, objects, etc.)\n\n4. **Depth Control**\n   - Prevents excessive recursion with depth limitations\n   - Mitigates stack overflow attacks through query parameter manipulation\n   - Maximum depth is controlled by `MAX_DEPTH` constant (default: 10)\n\n### Implementation Details\n\nThe sanitization process is applied at multiple levels:\n\n- During request configuration building\n- When processing URL path parameters\n- When serializing query parameters\n- When handling request and response interceptors\n- During retry and polling operations\n\nThis multi-layered approach ensures that all data passing through the library is properly sanitized, significantly reducing the risk of injection attacks and other security vulnerabilities.\n\n```typescript\n// Example of safe URL path parameter handling\nconst { data } = await api.getUser({\n  urlPathParams: {\n    id: 'user-id with spaces \u0026 special chars',\n  },\n  // Automatically encoded to: /users/user-id%20with%20spaces%20%26%20special%20chars\n});\n```\n\nSecurity is a core design principle of FetchFF, with sanitization mechanisms running automatically to provide protection without adding complexity to your code.\n\n\u003c/details\u003e\n\n## ⚛️ React Integration\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nFetchFF offers a high-performance React hook, `useFetcher(url, config)`, for efficient data fetching in React applications. This hook provides built-in caching, automatic request deduplication, comprehensive state management etc. Its API mirrors the native `fetch` and `fetchf(url, config)` signatures, allowing you to pass all standard and advanced configuration options seamlessly. Designed with React best practices in mind, `useFetcher` ensures optimal performance and a smooth developer experience.\n\nThe hook exposes base flags providing \u003cb\u003eintuitive\u003c/b\u003e mental model and improves UX defaults. The approach strikes a balance between simplicity and clarity, and avoids some of the confusion that arises with existing conventions in many other plugins. The other libraries toggle states too aggressively leading to poor performance.\n\n### Basic Usage\n\n```tsx\nimport { useFetcher } from 'fetchff/react';\n\nfunction UserProfile({ userId }: { userId: string }) {\n  const { data, error, isLoading, refetch } = useFetcher(\n    `/api/users/${userId}`,\n  );\n\n  if (isLoading) return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  if (error) return \u003cdiv\u003eError: {error.message}\u003c/div\u003e;\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003e{data.name}\u003c/h1\u003e\n      \u003cbutton onClick={refetch}\u003eRefresh\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n### Hook API\n\nThe `useFetcher(url, config)` hook returns an object with the following properties:\n\n- `data: ResponseData | null`  \n  The fetched data, typed as `T` (generic), or `null` if not available.\n- `error: ResponseError | null`  \n  Error object if the request failed, otherwise `null`.\n- `isLoading: boolean` (alias: `isFetching`)  \n  It is true when currently fetching (fetch is in progress) excluding background revalidations. The background revalidations simply update data.\n- `isFirstLoad: boolean`  \n  It is true when data is fetched for the first time, and there is no data available yet.\n- `isRefetching: boolean`  \n  It is true when a subsequent fetch is in progress (not during initial load).\n- `isSuccess: boolean`  \n  It is true if the last request completed successfully (2xx status codes).\n- `isError: boolean`  \n  It is true in cases of: non-2xx status code, network error, request failed, or response parsing error.\n- `config: RequestConfig`  \n  The configuration object used for the request.\n- `headers: Record\u003cstring, string\u003e`  \n  Response headers from the last successful request.\n- `refetch: (forceRefresh: boolean = true, config: RequestConfig = {}) =\u003e Promise\u003cFetchResponse\u003cResponseData, RequestBody, QueryParams, PathParams\u003e | null\u003e`  \n  Function to manually trigger a new request with the settings from the `useFetcher` hook.\n  - It always uses `softFail` strategy and returns a new FetchResponse object.\n  - The `forceRefresh` is `true` by default - it will bypass cache and force new request and cache refresh.\n  - The `config` helps to modify the request on-fly (e.g. add `body` to POST requests etc.). This is very useful for high performance dynamic updates without triggering re-renders on frontend.\n- `mutate: (data: ResponseData, settings: MutationSettings) =\u003e Promise\u003cFetchResponse\u003cResponseData, RequestBody, QueryParams, PathParams\u003e | null\u003e`  \n  Function to update cached data directly, by passing new data. The `settings` object contains currently `revalidate` (boolean) property. If set to `true`, a new request will be made after cache and component data are updated.\n\n### Configuration Options\n\nAll standard FetchFF options are supported, plus React-specific features:\n\n```tsx\nconst { data, error, isLoading } = useFetcher('/api/data', {\n  // Cache for 5 minutes\n  cacheTime: 300,\n\n  // Deduplicate requests within 2 seconds\n  dedupeTime: 2000,\n\n  // Revalidate when window regains focus\n  refetchOnFocus: true,\n\n  // Don't fetch immediately (useful for POST requests; React specific)\n  immediate: false,\n\n  // Custom error handling\n  strategy: 'softFail',\n\n  // Request configuration\n  method: 'POST',\n  body: { name: 'John' },\n  headers: { Authorization: 'Bearer token' },\n});\n```\n\n\u003e **Note on `immediate` behavior**: By default, only GET and HEAD requests (RFC 7231 safe methods) trigger automatically when the component mounts. Other HTTP methods like POST, PUT, DELETE require either setting `immediate: true` explicitly or calling `refetch()` manually. This prevents unintended side effects from automatic execution of non-safe HTTP operations.\n\n### Conditional Requests\n\nOnly fetch when conditions are met (`immediate` option is `true`):\n\n```tsx\nfunction ConditionalData({\n  shouldFetch,\n  userId,\n}: {\n  shouldFetch: boolean;\n  userId?: string;\n}) {\n  const { data, isLoading } = useFetcher(`/api/users/${userId}`, {\n    immediate: shouldFetch \u0026\u0026 !!userId,\n  });\n\n  // Will only fetch when shouldFetch is true and userId exists\n  return \u003cdiv\u003e{data ? data.name : 'No data'}\u003c/div\u003e;\n}\n```\n\nYou can also pass `null` as the URL to conditionally skip a request:\n\n```tsx\nfunction ConditionalData({\n  shouldFetch,\n  userId,\n}: {\n  shouldFetch: boolean;\n  userId?: string;\n}) {\n  const { data, isLoading } = useFetcher(\n    shouldFetch \u0026\u0026 userId ? `/api/users/${userId}` : null,\n  );\n\n  // Will only fetch when shouldFetch is true and userId exists\n  return \u003cdiv\u003e{data ? data.name : 'No data'}\u003c/div\u003e;\n}\n```\n\n\u003e **Note:** Passing `null` as the URL to conditionally skip a request is a legacy/deprecated approach (commonly used in SWR plugin). For new code, prefer using the `immediate` option for conditional fetching. The `null` URL method is still supported for backwards compatibility.\n\n### Dynamic URLs and Parameters\n\n```tsx\nfunction SearchResults({ query }: { query: string }) {\n  const { data, isLoading } = useFetcher('/api/search', {\n    params: { q: query, limit: 10 },\n    // Only fetch when query exists\n    immediate: !!query,\n  });\n\n  return (\n    \u003cdiv\u003e\n      {isLoading \u0026\u0026 \u003cdiv\u003eSearching...\u003c/div\u003e}\n      {data?.results?.map((item) =\u003e (\n        \u003cdiv key={item.id}\u003e{item.title}\u003c/div\u003e\n      ))}\n    \u003c/div\u003e\n  );\n}\n```\n\n### Mutations and Cache Updates\n\n```tsx\nfunction TodoList() {\n  const { data: todos = [], mutate, refetch } = useFetcher('/api/todos');\n\n  const addTodo = async (text: string) =\u003e {\n    // Optimistically update the cache\n    const newTodo = { id: Date.now(), text, completed: false };\n    mutate([...todos, newTodo]);\n\n    try {\n      // Make the actual request\n      await fetchf('/api/todos', {\n        method: 'POST',\n        body: { text },\n      });\n\n      // Revalidate to get the real data\n      refetch();\n    } catch (error) {\n      // Revert on error\n      mutate(todos);\n    }\n  };\n\n  return (\n    \u003cdiv\u003e\n      {todos?.map((todo) =\u003e (\n        \u003cdiv key={todo.id}\u003e{todo.text}\u003c/div\u003e\n      ))}\n      \u003cbutton onClick={() =\u003e addTodo('New todo')}\u003eAdd Todo\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n### Error Handling\n\n```tsx\nfunction DataWithErrorHandling() {\n  const { data, error, isLoading, refetch } = useFetcher('/api/data', {\n    retry: {\n      retries: 3,\n      delay: 1000,\n      backoff: 1.5,\n    },\n  });\n\n  if (isLoading) return \u003cdiv\u003eLoading...\u003c/div\u003e;\n\n  if (error) {\n    return (\n      \u003cdiv\u003e\n        \u003cp\u003eError: {error.message}\u003c/p\u003e\n        \u003cbutton onClick={refetch}\u003eTry Again\u003c/button\u003e\n      \u003c/div\u003e\n    );\n  }\n\n  return \u003cdiv\u003e{JSON.stringify(data)}\u003c/div\u003e;\n}\n```\n\n### Suspense Support\n\nUse with React Suspense for declarative loading states:\n\n```tsx\nimport { Suspense } from 'react';\n\nfunction DataComponent() {\n  const { data } = useFetcher('/api/data', {\n    strategy: 'reject', // Required for Suspense\n  });\n\n  return \u003cdiv\u003e{data.title}\u003c/div\u003e;\n}\n\nfunction App() {\n  return (\n    \u003cSuspense fallback={\u003cdiv\u003eLoading...\u003c/div\u003e}\u003e\n      \u003cDataComponent /\u003e\n    \u003c/Suspense\u003e\n  );\n}\n```\n\n### TypeScript Support\n\nFull TypeScript support with automatic type inference:\n\n```tsx\ninterface User {\n  id: number;\n  name: string;\n  email: string;\n}\n\ninterface UserParams {\n  include?: string[];\n}\n\nfunction UserComponent({ userId }: { userId: string }) {\n  const { data, error } = useFetcher\u003cUser\u003e(`/api/users/${userId}`, {\n    params: { include: ['profile', 'settings'] } as UserParams,\n  });\n\n  // data is automatically typed as User | null\n  // error is typed as ResponseError | null\n\n  return \u003cdiv\u003e{data?.name}\u003c/div\u003e;\n}\n```\n\n### Performance Features\n\n- **Automatic deduplication**: Multiple components requesting the same data share a single request\n- **Smart caching**: Configurable cache with automatic invalidation\n- **Minimal re-renders**: Optimized to prevent unnecessary component updates (relies on native React functionality)\n- **Background revalidation**: Keep data fresh without blocking the UI (use `staleTime` setting to control the time)\n\n### Best Practices\n\n1. **Use conditional requests** for dependent data:\n\n```tsx\nconst { data: user } = useFetcher('/api/user');\nconst { data: posts } = useFetcher(user ? `/api/users/${user.id}/posts` : null);\n```\n\n2. **Configure appropriate cache times** based on data volatility:\n\n```tsx\n// Static data - cache for 1 hour\nconst { data: config } = useFetcher('/api/config', { cacheTime: 3600 });\n\n// Dynamic data - cache for 30 seconds\nconst { data: feed } = useFetcher('/api/feed', { cacheTime: 30 });\n```\n\n3. **Use focus revalidation** for critical data:\n\n```tsx\nconst { data } = useFetcher('/api/critical-data', {\n  refetchOnFocus: true,\n});\n```\n\n4. **Handle loading and error states** appropriately:\n\n```tsx\nconst { data, error, isLoading } = useFetcher('/api/data');\n\nif (isLoading) return \u003cSpinner /\u003e;\nif (error) return \u003cErrorMessage error={error} /\u003e;\nreturn \u003cDataDisplay data={data} /\u003e;\n```\n\n5. **Leverage `staleTime` to control background revalidation:**\n\n```tsx\n// Data is considered fresh for 10 minutes; background revalidation happens after\nconst { data } = useFetcher('/api/notifications', { staleTime: 600 });\n```\n\n- Use a longer `staleTime` for rarely changing data to minimize unnecessary network requests.\n- Use a shorter `staleTime` for frequently updated data to keep the UI fresh.\n- Setting `staleTime: 0` disables the staleTime (default).\n- Combine `staleTime` with `cacheTime` for fine-grained cache and revalidation control.\n- Adjust `staleTime` per endpoint based on how critical or dynamic the data is.\n\n\u003c/details\u003e\n\n## Comparison with other libraries\n\n_fetchff uniquely combines advanced input sanitization, prototype pollution protection, unified cache across React and direct fetches, multiple error handling strategies, and a declarative API repository pattern—all in a single lightweight package._\n\n| Feature                                            | fetchff     | ofetch      | wretch       | axios        | native fetch() | swr             |\n| -------------------------------------------------- | ----------- | ----------- | ------------ | ------------ | -------------- | --------------- |\n| **Unified API Client**                             | ✅          | --          | --           | --           | --             | --              |\n| **Smart Request Cache**                            | ✅          | --          | --           | --           | --             | ✅              |\n| **Automatic Request Deduplication**                | ✅          | --          | --           | --           | --             | ✅              |\n| **Revalidation on Window Focus**                   | ✅          | --          | --           | --           | --             | ✅              |\n| **Custom Fetching Adapter**                        | ✅          | --          | --           | --           | --             | ✅              |\n| **Built-in Error Handling**                        | ✅          | --          | ✅           | --           | --             | --              |\n| **Customizable Error Handling**                    | ✅          | --          | ✅           | ✅           | --             | ✅              |\n| **Retries with exponential backoff**               | ✅          | --          | --           | --           | --             | --              |\n| **Advanced Query Params handling**                 | ✅          | --          | --           | --           | --             | --              |\n| **Custom Response Based Retry logic**              | ✅          | ✅          | ✅           | --           | --             | --              |\n| **Easy Timeouts**                                  | ✅          | ✅          | ✅           | ✅           | --             | --              |\n| **Adaptive Timeouts (Connection-aware)**           | ✅          | --          | --           | --           | --             | --              |\n| **Conditional Polling Functionality**              | ✅          | --          | --           | --           | --             | --              |\n| **Easy Cancellation of stale (previous) requests** | ✅          | --          | --           | --           | --             | --              |\n| **Default Responses**                              | ✅          | --          | --           | --           | --             | ✅              |\n| **Custom adapters (fetchers)**                     | ✅          | --          | --           | ✅           | --             | ✅              |\n| **Global Configuration**                           | ✅          | --          | ✅           | ✅           | --             | ✅              |\n| **TypeScript Support**                             | ✅          | ✅          | ✅           | ✅           | ✅             | ✅              |\n| **Built-in AbortController Support**               | ✅          | --          | --           | --           | --             | --              |\n| **Request Interceptors**                           | ✅          | ✅          | ✅           | ✅           | --             | --              |\n| **Safe deduping + cancellation**                   | ✅          | --          | --           | --           | --             | --              |\n| **Response-based polling decisions**               | ✅          | --          | --           | --           | --             | --              |\n| **Request/Response Data Transformation**           | ✅          | ✅          | ✅           | ✅           | --             | --              |\n| **Works with Multiple Frameworks**                 | ✅          | ✅          | ✅           | ✅           | ✅             | --              |\n| **Works across multiple instances or layers**      | ✅          | --          | --           | --           | --             | -- (only React) |\n| **Concurrent Request Deduplication**               | ✅          | --          | --           | --           | --             | ✅              |\n| **Flexible Error Handling Strategies**             | ✅          | --          | ✅           | ✅           | --             | ✅              |\n| **Dynamic URLs with Path and query separation**    | ✅          | --          | ✅           | --           | --             | --              |\n| **Automatic Retry on Failure**                     | ✅          | ✅          | --           | ✅           | --             | ✅              |\n| **Automatically handle 429 Retry-After headers**   | ✅          | --          | --           | --           | --             | --              |\n| **Built-in Input Sanitization**                    | ✅          | --          | --           | --           | --             | --              |\n| **Prototype Pollution Protection**                 | ✅          | --          | --           | --           | --             | --              |\n| **RFC 7231 Safe Methods Auto-execution**           | ✅          | --          | --           | --           | --             | --              |\n| **First Class React Integration**                  | ✅          | --          | --           | --           | --             | ✅              |\n| **Shared cache for React and direct fetches**      | ✅          | --          | --           | --           | --             | --              |\n| **Per-endpoint and per-request config merging**    | ✅          | --          | --           | --           | --             | --              |\n| **Declarative API repository pattern**             | ✅          | --          | --           | --           | --             | --              |\n| **Supports Server-Side Rendering (SSR)**           | ✅          | ✅          | ✅           | ✅           | ✅             | ✅              |\n| **SWR Pattern Support**                            | ✅          | --          | --           | --           | --             | ✅              |\n| **Revalidation on Tab Focus**                      | ✅          | --          | --           | --           | --             | ✅              |\n| **Revalidation on Network Reconnect**              | ✅          | --          | --           | --           | --             | ✅              |\n| **Minimal Installation Size**                      | 🟢 (5.2 KB) | 🟡 (6.5 KB) | 🟢 (2.21 KB) | 🔴 (13.7 KB) | 🟢 (0 KB)      | 🟡 (6.2 KB)     |\n\n## ✏️ Examples\n\nClick to expand particular examples below. You can also check [docs/examples/](./docs/examples/) for more examples of usage.\n\n### All Settings\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nHere’s an example of configuring and using the `createApiFetcher()` with all available settings.\n\n```typescript\nconst api = createApiFetcher({\n  baseURL: 'https://api.example.com/',\n  endpoints: {\n    getBooks: {\n      url: 'books/all',\n      method: 'get',\n      cancellable: true,\n      // All the global settings can be specified on per-endpoint basis as well\n    },\n  },\n  strategy: 'reject', // Error handling strategy.\n  cancellable: false, // If true, cancels previous requests to same endpoint.\n  rejectCancelled: false, // Reject promise for cancelled requests.\n  flattenResponse: false, // If true, flatten nested response data.\n  defaultResponse: null, // Default response when there is no data or endpoint fails.\n  withCredentials: true, // Pass cookies to all requests.\n  timeout: 30000, // Request timeout in milliseconds. Defaults to 30s (60s on slow connections), can be overridden.\n  dedupeTime: 0, // Time window, in milliseconds, during which identical requests are deduplicated (treated as single request).\n  immediate: false, // If false, disables automatic request on initialization (useful for POST or conditional requests, React-specific)\n  staleTime: 600, // Data is considered fresh for 10 minutes before background revalidation (disabled by default)\n  pollingInterval: 5000, // Interval in milliseconds between polling attempts. Setting 0 disables polling.\n  pollingDelay: 1000, // Wait 1 second before beginning each polling attempt\n  maxPollingAttempts: 10, // Stop polling after 10 attempts\n  shouldStopPolling: (response, attempt) =\u003e false, // Function to determine if polling should stop based on the response. Return true to stop polling, or false to continue.\n  method: 'get', // Default request method.\n  params: {}, // Default params added to all requests.\n  urlPathParams: {}, // Dynamic URL path parameters for replacing segments like /user/:id\n  data: {}, // Alias for 'body'. Default data passed to POST, PUT, DELETE and PATCH requests.\n  cacheTime: 300, // Cache time in seconds. In this case it is valid for 5 minutes (300 seconds)\n  cacheKey: (config) =\u003e `${config.url}-${config.method}`, // Custom cache key based on URL and method\n  cacheBuster: (config) =\u003e config.method === 'POST', // Bust cache for POST requests\n  skipCache: (response, config) =\u003e response.status !== 200, // Skip caching on non-200 responses\n  cacheErrors: false, // Cache error responses as well as successful ones, default false\n  onError(error) {\n    // Interceptor on error\n    console.error('Request failed', error);\n  },\n  async onRequest(config) {\n    // Interceptor on each request\n    console.error('Fired on each request', config);\n  },\n  async onResponse(response) {\n    // Interceptor on each response\n    console.error('Fired on each response', response);\n  },\n  logger: {\n    // Custom logger for logging errors.\n    error(...args) {\n      console.log('My custom error log', ...args);\n    },\n    warn(...args) {\n      console.log('My custom warning log', ...args);\n    },\n  },\n  retry: {\n    retries: 3, // Number of retries on failure.\n    delay: 1000, // Initial delay between retries in milliseconds. Defaults to 1s (2s on slow connections), can be overridden.\n    backoff: 1.5, // Backoff factor for retry delay.\n    maxDelay: 30000, // Maximum delay between retries in milliseconds. Defaults to 30s (60s on slow connections), can be overridden.\n    resetTimeout: true, // Reset the timeout when retrying requests.\n    retryOn: [408, 409, 425, 429, 500, 502, 503, 504], // HTTP status codes to retry on.\n    shouldRetry: async (response, attempts) =\u003e {\n      // Custom retry logic.\n      return (\n        attempts \u003c 3 \u0026\u0026\n        [408, 500, 502, 503, 504].includes(response.error.status)\n      );\n    },\n  },\n});\n\ntry {\n  // The same API config as used above, except the \"endpoints\" and \"fetcher\" and fetcher could be used as 3rd argument of the api.getBooks()\n  const { data } = await api.getBooks();\n  console.log('Request succeeded:', data);\n} catch (error) {\n  console.error('Request ultimately failed:', error);\n}\n```\n\n\u003c/details\u003e\n\n### Examples Using \u003ci\u003ecreateApiFetcher()\u003c/i\u003e\n\nAll examples below are with usage of `createApiFetcher()`. You can also use `fetchf()` independently.\n\n#### Multiple APIs Handler from different API sources\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\n// Create fetcher instance\nconst api = createApiFetcher({\n  baseURL: 'https://example.com/api/v1',\n  endpoints: {\n    sendMessage: {\n      method: 'post',\n      url: '/send-message/:postId',\n    },\n    getMessage: {\n      url: '/get-message/',\n      // Change baseURL to external for this endpoint only\n      baseURL: 'https://externalprovider.com/api/v2',\n    },\n  },\n});\n\n// Make a wrapper function and call your API\nasync function sendAndGetMessage() {\n  await api.sendMessage({\n    body: { message: 'Text' },\n    urlPathParams: { postId: 1 },\n  });\n\n  const { data } = await api.getMessage({\n    params: { postId: 1 },\n  });\n}\n\n// Invoke your wrapper function\nsendAndGetMessage();\n```\n\n\u003c/details\u003e\n\n#### Using with Full TypeScript Support\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nThe library includes all necessary [TypeScript](http://typescriptlang.org) definitions bringing full TypeScript support to your API Handler. The package ships interfaces with sensible defaults making it easier to add new endpoints.\n\n```typescript\n// books.d.ts\ninterface Book {\n  id: number;\n  title: string;\n  rating: number;\n}\n\ninterface Books {\n  books: Book[];\n  totalResults: number;\n}\n\ninterface BookQueryParams {\n  newBook?: boolean;\n  category?: string;\n}\n\ninterface BookPathParams {\n  bookId: number;\n}\n```\n\n```typescript\n// api.ts\nimport type { Endpoint } from 'fetchff';\nimport { createApiFetcher } from 'fetchff';\n\nconst endpoints = {\n  fetchBooks: {\n    url: '/books',\n    method: 'GET' as const,\n  },\n  fetchBook: {\n    url: '/books/:bookId',\n    method: 'GET' as const,\n  },\n} as const;\n\n// Define endpoints with proper typing\ninterface EndpointTypes {\n  fetchBook: Endpoint\u003c{\n    response: Book;\n    params: BookQueryParams;\n    urlPathParams: BookPathParams;\n  }\u003e;\n  fetchBooks: Endpoint\u003c{ response: Books; params: BookQueryParams }\u003e;\n}\n\nconst api = createApiFetcher\u003cEndpointTypes\u003e({\n  baseURL: 'https://example.com/api',\n  strategy: 'softFail',\n  endpoints,\n});\n\nexport { api };\nexport type { Book, Books, BookQueryParams, BookPathParams };\n```\n\n```typescript\n// Usage with full type safety\nimport { api, type Book, type Books } from './api';\n\n// Properly typed request with URL params\nconst book = await api.fetchBook({\n  params: { newBook: true },\n  urlPathParams: { bookId: 1 },\n});\n\nif (book.error) {\n  console.error('Failed to fetch book:', book.error.message);\n} else {\n  console.log('Book title:', book.data?.title);\n}\n\n// For example, this will cause a TypeScript error as 'rating' doesn't exist in BookQueryParams\n// const invalidBook = await api.fetchBook({\n//   params: { rating: 5 }\n// });\n\n// Generic type can be passed directly for additional type safety\nconst books = await api.fetchBooks\u003cBooks\u003e({\n  params: { category: 'fiction' },\n});\n\nif (books.error) {\n  console.error('Failed to fetch books:', books.error.message);\n} else {\n  console.log('Total books:', books.data?.totalResults);\n}\n```\n\n\u003c/details\u003e\n\n#### Using with TypeScript and Custom Headers\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\nconst endpoints = {\n  getPosts: {\n    url: '/posts/:subject',\n  },\n  getUser: {\n    // Generally there is no need to specify method: 'get' for GET requests as it is default one. It can be adjusted using global \"method\" setting\n    method: 'get',\n    url: '/user-details',\n  },\n  updateUserDetails: {\n    method: 'post',\n    url: '/user-details/update/:userId',\n    strategy: 'defaultResponse',\n  },\n};\n\ninterface PostsResponse {\n  posts: Array\u003c{ id: number; title: string; content: string }\u003e;\n  totalCount: number;\n}\n\ninterface PostsQueryParams {\n  additionalInfo?: string;\n  limit?: number;\n}\n\ninterface PostsPathParams {\n  subject: string;\n}\n\ninterface EndpointTypes {\n  getPosts: Endpoint\u003c{\n    response: PostsResponse;\n    params: PostsQueryParams;\n    urlPathParams: PostsPathParams;\n  }\u003e;\n}\n\nconst api = createApiFetcher\u003cEndpointTypes\u003e({\n  baseURL: 'https://example.com/api',\n  endpoints,\n  onError(error) {\n    console.log('Request failed', error);\n  },\n  headers: {\n    'my-auth-key': 'example-auth-key-32rjjfa',\n  },\n});\n\n// Fetch user data - \"data\" will return data directly\n// GET to: https://example.com/api/user-details?userId=1\u0026ratings[]=1\u0026ratings[]=2\nconst { data } = await api.getUser({\n  params: { userId: 1, ratings: [1, 2] },\n});\n\n// Fetch posts - \"data\" will return data directly\n// GET to: https://example.com/api/posts/test?additionalInfo=something\nconst { data: postsData } = await api.getPosts({\n  params: { additionalInfo: 'something' },\n  urlPathParams: { subject: 'test' },\n});\n\n// Send POST request to update userId \"1\"\nawait api.updateUserDetails({\n  body: { name: 'Mark' },\n  urlPathParams: { userId: 1 },\n});\n\n// Send POST request to update array of user ratings for userId \"1\"\nawait api.updateUserDetails({\n  body: { name: 'Mark', ratings: [1, 2] },\n  urlPathParams: { userId: 1 },\n});\n```\n\nIn the example above we fetch data from an API for user with an ID of 1. We also make a GET request to fetch some posts, update user's name to Mark. If you want to use more strict typings, please check TypeScript Usage section below.\n\n\u003c/details\u003e\n\n#### Custom Fetcher\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\n// Create the API fetcher with the custom fetcher\nconst api = createApiFetcher({\n  baseURL: 'https://api.example.com/',\n  retry: retryConfig,\n  // This function will be called whenever a request is being fired.\n  async fetcher(config) {\n    // Implement your custom fetch logic here\n    const response = await fetch(config.url, config);\n    // Optionally, process or transform the response\n    return response;\n  },\n  endpoints: {\n    getBooks: {\n      url: 'books/all',\n      method: 'get',\n      cancellable: true,\n      // All the global settings can be specified on per-endpoint basis as well\n    },\n  },\n});\n```\n\n\u003c/details\u003e\n\n#### Error handling strategy - `reject` (default)\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\nconst api = createApiFetcher({\n  baseURL: 'https://example.com/api',\n  endpoints: {\n    sendMessage: {\n      method: 'post',\n      url: '/send-message/:postId',\n      strategy: 'reject', // It is a default strategy so it does not really need to be here\n    },\n  },\n});\n\nasync function sendMessage() {\n  try {\n    await api.sendMessage({\n      body: { message: 'Text' },\n      urlPathParams: { postId: 1 },\n    });\n\n    console.log('Message sent successfully');\n  } catch (error) {\n    console.error('Message failed to send:', error.message);\n  }\n}\n\nsendMessage();\n```\n\n\u003c/details\u003e\n\n#### Error handling strategy - `softFail`\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\nconst api = createApiFetcher({\n  baseURL: 'https://example.com/api',\n  endpoints: {\n    sendMessage: {\n      method: 'post',\n      url: '/send-message/:postId',\n      strategy: 'softFail', // Returns a response object with additional error details without rejecting the promise.\n    },\n  },\n});\n\nasync function sendMessage() {\n  const { data, error } = await api.sendMessage({\n    body: { message: 'Text' },\n    urlPathParams: { postId: 1 },\n  });\n\n  if (error) {\n    console.error('Request Error', error.message);\n  } else {\n    console.log('Message sent successfully:', data);\n  }\n}\n\nsendMessage();\n```\n\n\u003c/details\u003e\n\n#### Error handling strategy - `defaultResponse`\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\nconst api = createApiFetcher({\n  baseURL: 'https://example.com/api',\n  endpoints: {\n    sendMessage: {\n      method: 'post',\n      url: '/send-message/:postId',\n      // You can also specify strategy and other settings in global list of endpoints, but just for this endpoint\n      // strategy: 'defaultResponse',\n    },\n  },\n});\n\nasync function sendMessage() {\n  const { data, error } = await api.sendMessage({\n    body: { message: 'Text' },\n    urlPathParams: { postId: 1 },\n    strategy: 'defaultResponse',\n    // null is a default setting, you can change it to empty {} or anything\n    defaultResponse: { status: 'failed', message: 'Default response' },\n    onError(error) {\n      // Callback is still triggered here\n      console.error('API error:', error.message);\n    },\n  });\n\n  if (error) {\n    console.warn('Message failed to send, using default response:', data);\n    return;\n  }\n\n  console.log('Message sent successfully:', data);\n}\n\nsendMessage();\n```\n\n\u003c/details\u003e\n\n#### Error handling strategy - `silent`\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\nconst api = createApiFetcher({\n  baseURL: 'https://example.com/api',\n  endpoints: {\n    sendMessage: {\n      method: 'post',\n      url: '/send-message/:postId',\n      // You can also specify strategy and other settings in here, just for this endpoint\n      // strategy: 'silent',\n    },\n  },\n});\n\nasync function sendMessage() {\n  await api.sendMessage({\n    body: { message: 'Text' },\n    urlPathParams: { postId: 1 },\n    strategy: 'silent',\n    onError(error) {\n      console.error('Silent error logged:', error.message);\n    },\n  });\n\n  // Because of the strategy, if API call fails, it will never reach this point. Otherwise try/catch would need to be required.\n  console.log('Message sent successfully');\n}\n\n// Note that since strategy is \"silent\" and sendMessage should not be awaited anywhere\nsendMessage();\n```\n\n\u003c/details\u003e\n\n#### `onError` Interceptor\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\nconst api = createApiFetcher({\n  baseURL: 'https://example.com/api',\n  endpoints: {\n    sendMessage: {\n      method: 'post',\n      url: '/send-message/:postId',\n    },\n  },\n});\n\nasync function sendMessage() {\n  try {\n    await api.sendMessage({\n      body: { message: 'Text' },\n      urlPathParams: { postId: 1 },\n      onError(error) {\n        console.error('Error intercepted:', error.message);\n        console.error('Response:', error.response);\n        console.error('Config:', error.config);\n      },\n    });\n\n    console.log('Message sent successfully');\n  } catch (error) {\n    console.error('Final error handler:', error.message);\n  }\n}\n\nsendMessage();\n```\n\n\u003c/details\u003e\n\n#### Request Chaining\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nIn this example, we make an initial request to get a user's details, then use that data to fetch additional information in a subsequent request. This pattern allows you to perform multiple asynchronous operations in sequence, using the result of one request to drive the next.\n\n```typescript\nimport { createApiFetcher } from 'fetchff';\n\n// Initialize API fetcher with endpoints\nconst api = createApiFetcher({\n  baseURL: 'https://example.com/api',\n  endpoints: {\n    getUser: { url: '/user' },\n    createPost: { url: '/post', method: 'POST' },\n  },\n});\n\nasync function fetchUserAndCreatePost(userId: number, postData: any) {\n  // Fetch user data\n  const { data: userData } = await api.getUser({ params: { userId } });\n\n  // Create a new post with the fetched user data\n  return await api.createPost({\n    body: {\n      ...postData,\n      userId: userData.id, // Use the user's ID from the response\n    },\n  });\n}\n\n// Example usage\nfetchUserAndCreatePost(1, { title: 'New Post', content: 'This is a new post.' })\n  .then((response) =\u003e console.log('Post created:', response))\n  .catch((error) =\u003e console.error('Error:', error));\n```\n\n\u003c/details\u003e\n\n### Example Usage in Node.js\n\n#### Using with Express.js / Fastify\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```ts\nimport { fetchf } from 'fetchff';\n\napp.get('/api/proxy', async (req, res) =\u003e {\n  const { data, error } = await fetchf('https://external.api/resource');\n  if (error) {\n    return res.status(error.status).json({ error: error.message });\n  }\n  res.json(data);\n});\n```\n\n\u003c/details\u003e\n\n### Example Usage with Frameworks and Libraries\n\n`fetchff` is designed to seamlessly integrate with any popular frameworks like Next.js, libraries like React, Vue, React Query and SWR. It is written in pure JS so you can effortlessly manage API requests with minimal setup, and without any dependencies.\n\n#### Advanced Caching Strategies\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```typescript\nimport { fetchf, mutate, deleteCache } from 'fetchff';\n\n// Example: User dashboard with smart caching\nconst fetchUserDashboard = async (userId: string) =\u003e {\n  return await fetchf(`/api/users/${userId}/dashboard`, {\n    cacheTime: 300, // Cache for 5 minutes\n    staleTime: 60, // Background revalidate after 1 minute\n    cacheKey: `user-dashboard-${userId}`, // Custom cache key\n    skipCache: (response) =\u003e response.status === 503, // Skip caching on service unavailable\n    refetchOnFocus: true, // Refresh when user returns to tab\n  });\n};\n\n// Example: Optimistic updates with cache mutations\nconst updateUserProfile = async (userId: string, updates: any) =\u003e {\n  // Optimistically update cache\n  const currentData = await fetchf(`/api/users/${userId}`);\n  await mutate(`/api/users/${userId}`, { ...currentData.data, ...updates });\n\n  try {\n    // Make actual API call\n    const response = await fetchf(`/api/users/${userId}`, {\n      method: 'PATCH',\n      body: updates,\n    });\n\n    // Update cache with real response\n    await mutate(`/api/users/${userId}`, response.data, { revalidate: true });\n\n    return response;\n  } catch (error) {\n    // Revert cache on error\n    await mutate(`/api/users/${userId}`, currentData.data);\n    throw error;\n  }\n};\n\n// Example: Cache invalidation after user logout\nconst logout = async () =\u003e {\n  await fetchf('/api/auth/logout', { method: 'POST' });\n\n  // Clear all user-related cache\n  deleteCache('/api/user*');\n  deleteCache('/api/dashboard*');\n};\n```\n\n\u003c/details\u003e\n\n#### Real-time Polling Implementation\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```typescript\nimport { fetchf } from 'fetchff';\n\n// Example: Job status monitoring with intelligent polling\nconst monitorJobStatus = async (jobId: string) =\u003e {\n  return await fetchf(`/api/jobs/${jobId}/status`, {\n    pollingInterval: 2000, // Poll every 2 seconds\n    pollingDelay: 500, // Wait 500ms before first poll\n    maxPollingAttempts: 30, // Max 30 attempts (1 minute total)\n\n    shouldStopPolling(response, attempt) {\n      // Stop polling when job is complete or failed\n      if (\n        response.data?.status === 'completed' ||\n        response.data?.status === 'failed'\n      ) {\n        return true;\n      }\n\n      // Stop if we've been polling for too long\n      if (attempt \u003e= 30) {\n        console.warn('Job monitoring timeout after 30 attempts');\n        return true;\n      }\n\n      return false;\n    },\n\n    onResponse(response) {\n      console.log(`Job ${jobId} status:`, response.data?.status);\n\n      // Update UI progress if available\n      if (response.data?.progress) {\n        updateProgressBar(response.data.progress);\n      }\n    },\n  });\n};\n\n// Example: Server health monitoring\nconst monitorServerHealth = async () =\u003e {\n  return await fetchf('/api/health', {\n    pollingInterval: 30000, // Check every 30 seconds\n    shouldStopPolling(response, attempt) {\n      // Never stop health monitoring (until manually cancelled)\n      return false;\n    },\n\n    onResponse(response) {\n      const isHealthy = response.data?.status === 'healthy';\n      updateHealthIndicator(isHealthy);\n\n      if (!isHealthy) {\n        console.warn('Server health check failed:', response.data);\n        notifyAdmins(response.data);\n      }\n    },\n\n    onError(error) {\n      console.error('Health check failed:', error.message);\n      updateHealthIndicator(false);\n    },\n  });\n};\n\n// Helper functions (implementation depends on your UI framework)\nfunction updateProgressBar(progress: number) {\n  // Update progress bar in UI\n  console.log(`Progress: ${progress}%`);\n}\n\nfunction updateHealthIndicator(isHealthy: boolean) {\n  // Update health indicator in UI\n  console.log(`Server status: ${isHealthy ? 'Healthy' : 'Unhealthy'}`);\n}\n\nfunction notifyAdmins(healthData: any) {\n  // Send notifications to administrators\n  console.log('Notifying admins about health issue:', healthData);\n}\n```\n\n\u003c/details\u003e\n\n#### Using with React Query\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nIntegrate `fetchff` with React Query to streamline your data fetching:\n\n\u003e **Note:** Official support for `useFetcher(url, config)` is here. Check React Integration section above to get an idea how to use it instead of SWR.\n\n```tsx\nimport { createApiFetcher } from 'fetchff';\nimport { useQuery } from '@tanstack/react-query';\n\nconst api = createApiFetcher({\n  baseURL: 'https://example.com/api',\n  endpoints: {\n    getProfile: {\n      url: '/profile/:id',\n    },\n  },\n});\n\nexport const useProfile = (id: string) =\u003e {\n  return useQuery({\n    queryKey: ['profile', id],\n    queryFn: () =\u003e api.getProfile({ urlPathParams: { id } }),\n    enabled: !!id, // Only fetch when id exists\n  });\n};\n```\n\n\u003c/details\u003e\n\n#### Using with SWR\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\nCombine `fetchff` with SWR for efficient data fetching and caching.\n\n\u003e **Note:** Official support for `useFetcher(url, config)` is here. Check React Integration section above to get an idea how to use it instead of SWR.\n\nSingle calls:\n\n```typescript\nimport { fetchf } from 'fetchff';\nimport useSWR from 'swr';\n\nconst fetchProfile = (id: string) =\u003e\n  fetchf(`https://example.com/api/profile/${id}`, {\n    strategy: 'softFail',\n  });\n\nexport const useProfile = (id: string) =\u003e {\n  const { data, error } = useSWR(id ? ['profile', id] : null, () =\u003e\n    fetchProfile(id),\n  );\n\n  return {\n    profile: data?.data,\n    isLoading: !error \u0026\u0026 !data,\n    isError: error || data?.error,\n  };\n};\n```\n\nMany endpoints:\n\n```tsx\nimport { createApiFetcher } from 'fetchff';\nimport useSWR from 'swr';\n\nconst api = createApiFetcher({\n  baseURL: 'https://example.com/api',\n  endpoints: {\n    getProfile: {\n      url: '/profile/:id',\n    },\n  },\n});\n\nexport const useProfile = (id: string) =\u003e {\n  const { data, error } = useSWR(id ? ['profile', id] : null, () =\u003e\n    api.getProfile({ urlPathParams: { id } }),\n  );\n\n  return {\n    profile: data?.data,\n    isLoading: !error \u0026\u0026 !data,\n    isError: error || data?.error,\n  };\n};\n```\n\n\u003c/details\u003e\n\n#### 🌊 Using with Vue\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n  \u003cbr\u003e\n\n```typescript\n// src/api.ts\nimport { createApiFetcher } from 'fetchff';\n\nconst api = createApiFetcher({\n  baseURL: 'https://example.com/api',\n  strategy: 'softFail',\n  endpoints: {\n    getProfile: { url: '/profile/:id' },\n  },\n});\n\nexport default api;\n```\n\n```typescript\n// src/composables/useProfile.ts\nimport { ref, onMounted } from 'vue';\nimport api from '../api';\n\nexport function useProfile(id: number) {\n  const profile = ref(null);\n  const isLoading = ref(true);\n  const isError = ref(null);\n\n  const fetchProfile = async () =\u003e {\n    const { data, error } = await api.getProfile({ urlPathParams: { id } });\n\n    if (error) isError.value = error;\n    else if (data) profile.value = data;\n\n    isLoading.value = false;\n  };\n\n  onMounted(fetchProfile);\n\n  return { profile, isLoading, isError };\n}\n```\n\n```html\n\u003c!-- src/components/Profile.vue --\u003e\n\u003ctemplate\u003e\n  \u003cdiv\u003e\n    \u003ch1\u003eProfile\u003c/h1\u003e\n    \u003cdiv v-if=\"isLoading\"\u003eLoading...\u003c/div\u003e\n    \u003cdiv v-if=\"isError\"\u003eError: {{ isError.message }}\u003c/div\u003e\n    \u003cdiv v-if=\"profile\"\u003e\n      \u003cp\u003eName: {{ profile.name }}\u003c/p\u003e\n      \u003cp\u003eEmail: {{ profile.email }}\u003c/p\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript lang=\"ts\"\u003e\n  import { defineComponent } from 'vue';\n  import { useProfile } from '../composables/useProfile';\n\n  export default defineComponent({\n    props: { id: Number },\n    setup(props) {\n      return useProfile(props.id);\n    },\n  });\n\u003c/script\u003e\n```\n\n\u003c/details\u003e\n\n## 🛠️ Compatibility and Polyfills\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cspan style=\"cursor:pointer\"\u003eClick to expand\u003c/span\u003e\u003c/summary\u003e\n\n### Compatibility\n\nWhile `fetchff` is designed to work seamlessly with modern environments (ES2018+), some older browsers or specific edge cases might require additional support.\n\nCurrently, `fetchff` offers three types of builds:\n\n1. \u003cb\u003eBrowser ESM build (.mjs):\u003c/b\u003e Ideal for modern browsers and module-based environments (when you use the [type=\"module\"](https://caniuse.com/?search=type%3D%22module%22) attribute).\n   Location: `dist/browser/index.mjs`\n   Compatibility: `ES2018+`\n\n2. \u003cb\u003eStandard Browser build:\u003c/b\u003e A global UMD bundle, compatible with older browsers.\n   Location: `dist/browser/index.global.js`\n   Compatibility: `ES2018+`\n\n3. \u003cb\u003eNode.js CJS build:\u003c/b\u003e Designed for Node.js environments that rely on CommonJS modules.\n   Location: `dist/node/index.js`\n   Compatibility: `Node.js 18+`\n\nFor projects that need to support older browsers, especially those predating ES2018, additional polyfills or transpilation may be necessary. Consider using tools like Babel, SWC or core-js to ensure compatibility with environments that do not natively support ES2018+ features. Bundlers like Webpack or Rollup usually handle these concerns out of the box.\n\nYou can check [Can I Use ES2018](https://github.com/github/fetch) to verify browser support for specific ES2018 features.\n\n### Polyfills\n\nFor environments that do not support modern JavaScript features or APIs, you might need to include polyfills. Some common polyfills include:\n\n- **Fetch Polyfill**: For environments that do not support the native `fetch` API. You can use libraries like [whatwg-fetch](https://github.com/github/fetch) to provide a fetch implementation.\n- **Promise Polyfill**: For older browsers that do not support Promises. Libraries like [es6-promise](https://github.com/stefanpenner/es6-promise) can be used.\n- **AbortController Polyfill**: For environments that do not support the `AbortController` API used for aborting fetch requests. You can use the [abort-controller](https://github.com/mysticatea/abort-controller) polyfill.\n\n### React Native\n\n`fetchff` is fully compatible with React Native. Core features like caching, retries, deduplication, and the React hook work out of the box.\n\nTo enable `refetchOnFocus` and `refetchOnReconnect`, register event providers at your app's entry point using `setEventProvider()`:\n\n```ts\nimport { AppState } from 'react-native';\nimport NetInfo from '@react-native-community/netinfo';\nimport { setEventProvider } from 'fetchff';\n\n// Refetch when app comes to foreground\nsetEventProvider('focus', (handler) =\u003e {\n  const sub = AppState.addEventListener('change', (state) =\u003e {\n    if (state === 'active') handler();\n  });\n  return () =\u003e sub.remove();\n});\n\n// Refetch when network reconnects\nsetEventProvider('online', (handler) =\u003e {\n  let wasConnected = true;\n  const unsubscribe = NetInfo.addEventListener((state) =\u003e {\n    if (state.isConnected \u0026\u0026 !wasConnected) handler();\n    wasConnected = !!state.isConnected;\n  });\n  return unsubscribe;\n});\n```\n\n\u003e **Note:** `@react-native-community/netinfo` is optional — only needed if you use `refetchOnReconnect`.\n\n### Using `node-fetch` for Node.js \u003c 18\n\nIf you need to support Node.js versions below 18 (not officially supported), you can use the [`node-fetch`](https://www.npmjs.com/package/node-fetch) package to polyfill the `fetch` API. Install it with:\n\n```bash\nnpm install node-fetch\n```\n\nThen, at the entry point of your application, add:\n\n```js\nglobalThis.fetch = require('node-fetch');\n```\n\n\u003e **Note:** Official support is for Node.js 18 and above. Using older Node.js versions is discouraged and may result in unexpected issues.\n\n\u003c/details\u003e\n\n## ✔️ Support and collaboration\n\nIf you have any idea for an improvement, please file an issue. Feel free to make a PR if you are willing to collaborate on the project. Thank you :)\n","funding_links":["https://patreon.com/mattccc"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmattccc%2Ffetchff","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmattccc%2Ffetchff","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmattccc%2Ffetchff/lists"}