{"id":14537704,"url":"https://github.com/suhaotian/xior","last_synced_at":"2025-05-15T08:10:10.128Z","repository":{"id":223388729,"uuid":"760187472","full_name":"suhaotian/xior","owner":"suhaotian","description":"A liteweight fetch wrapper with plugins support and similar API to axios.","archived":false,"fork":false,"pushed_at":"2025-04-30T23:16:08.000Z","size":1619,"stargazers_count":337,"open_issues_count":3,"forks_count":7,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-05-10T16:02:47.920Z","etag":null,"topics":["ajax","axios-api","fetch","fetch-wrapper","http","plugins","typescript","xior"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/xior","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/suhaotian.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-02-20T00:14:38.000Z","updated_at":"2025-05-09T18:41:03.000Z","dependencies_parsed_at":"2024-03-16T10:38:06.011Z","dependency_job_id":"3f143c3d-f5a8-417c-8f49-528158b806e6","html_url":"https://github.com/suhaotian/xior","commit_stats":null,"previous_names":["suhaotian/xior"],"tags_count":54,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/suhaotian%2Fxior","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/suhaotian%2Fxior/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/suhaotian%2Fxior/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/suhaotian%2Fxior/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/suhaotian","download_url":"https://codeload.github.com/suhaotian/xior/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253441848,"owners_count":21909196,"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","axios-api","fetch","fetch-wrapper","http","plugins","typescript","xior"],"created_at":"2024-09-05T10:01:27.741Z","updated_at":"2025-05-15T08:10:09.935Z","avatar_url":"https://github.com/suhaotian.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"[![Build](https://github.com/suhaotian/xior/actions/workflows/check.yml/badge.svg)](https://github.com/suhaotian/xior/actions/workflows/check.yml)\n[![GitHub Issues](https://img.shields.io/github/issues-closed/suhaotian/xior)](https://github.com/suhaotian/xior/issues)\n[![Size](https://deno.bundlejs.com/badge?q=xior@0.7.8\u0026badge=detailed\u0026treeshake=%5B%7B+default+%7D%5D)](https://bundlejs.com/?q=xior%400.7.8\u0026treeshake=%5B%7B+default+%7D%5D)\n[![NPM Version](https://badgen.net/npm/v/xior?color=green)](https://www.npmjs.com/package/xior)\n![NPM Weekly Downloads](https://img.shields.io/npm/dw/xior)\n![NPM Month Downloads](https://img.shields.io/npm/dm/xior.svg?style=flat)\n![typescript](https://badgen.net/badge/icon/typescript?icon=typescript\u0026label\u0026color=blue)\n\n## Intro\n\nA lite http request lib based on **fetch** with plugin support and similar API to axios.\n\n**Features:**\n\n- 🔥 Use **fetch**\n- 🫡 **Similar axios API**: `axios.create` / `axios.interceptors` / `.get/post/put/patch/delete/head/options`\n- 🤙 Supports timeout, canceling requests, and **nested query encoding**\n- 🥷 Supports **plugins**: **error retry**, deduplication, throttling, **cache**, error cache, mock, and custom plugins\n- 🚀 Lightweight (~6KB, Gzip ~3kb)\n- 👊 Unit tested and strongly typed 💪\n\n## Table of Contents\n\n- [Intro](#intro)\n- [Table of Contents](#table-of-contents)\n- [Getting Started](#getting-started)\n  - [Installing](#installing)\n    - [Package manager](#package-manager)\n    - [Use CDN](#use-cdn)\n  - [Create instance](#create-instance)\n  - [GET / POST / DELETE / PUT / PATCH / OPTIONS / HEAD](#get--post--delete--put--patch--options--head)\n  - [Change default headers or params](#change-default-headers-or-params)\n  - [Get response headers](#get-response-headers)\n  - [Upload file](#upload-file)\n  - [Using interceptors](#using-interceptors)\n  - [Cleanup interceptors](#cleanup-interceptors)\n  - [Timeout and Cancel request](#timeout-and-cancel-request)\n  - [Proxy or use custom fetch implementations](#proxy-or-use-custom-fetch-implementations)\n  - [Custom data parser](#custom-data-parser)\n  - [Encrypt and Decrypt Example](#encrypt-and-decrypt-example)\n  - [Tips: Make your SSR(Server-side Rendering) app more stable and faster](#tips-make-your-ssrserver-side-rendering-app-more-stable-and-faster)\n- [Plugins](#plugins)\n  - [Error retry plugin](#error-retry-plugin)\n  - [Request throttle plugin](#request-throttle-plugin)\n  - [Request dedupe plugin](#request-dedupe-plugin)\n  - [Error cache plugin](#error-cache-plugin)\n  - [Cache plugin](#cache-plugin)\n  - [Persist cache data](#persist-cache-data)\n  - [Upload and download progress plugin](#upload-and-download-progress-plugin)\n  - [Mock plugin](#mock-plugin)\n  - [Auth refresh token plugin(from community)](#auth-refresh-token-pluginfrom-community)\n  - [Auth refresh token plugin(built-in)](#auth-refresh-token-pluginbuilt-in)\n  - [Create your own custom plugin](#create-your-own-custom-plugin)\n  - [Cleanup plugins example](#cleanup-plugins-example)\n- [Helper functions](#helper-functions)\n- [FAQ](#faq)\n  - [1. Is **xior** 100% compatiable with `axios`?](#1-is-xior-100-compatiable-with-axios)\n  - [2. Can I use xior in projects like Bun, Expo, React Native, RemixJS, Next.js, Vue, Nuxt.js, Tauri or `NervJS/Taro`?](#2-can-i-use-xior-in-projects-like-bun-expo-react-native-remixjs-nextjs-vue-nuxtjs-tauri-or-nervjstaro)\n  - [3. How can I use custom fetch implementation or How to support **proxy** feature?](#3-how-can-i-use-custom-fetch-implementation-or-how-to-support-proxy-feature)\n  - [4. How do I handle responses with types like `'stream'`, `'document'`, `'arraybuffer'`, or `'blob'`?](#4-how-do-i-handle-responses-with-types-like-stream-document-arraybuffer-or-blob)\n  - [5. How do I support older browsers?](#5-how-do-i-support-older-browsers)\n  - [6. Why is xior named \"xior\"?](#6-why-is-xior-named-xior)\n  - [7. Where can I ask additional questions?](#7-where-can-i-ask-additional-questions)\n- [Migrate from `axios` to **xior**](#migrate-from-axios-to-xior)\n  - [GET](#get)\n  - [POST](#post)\n  - [`axios(requestObj)`: axios({ method: 'get', params: { a: 1 } })](#axiosrequestobj-axios-method-get-params--a-1--)\n  - [Creating an instance](#creating-an-instance)\n  - [Get response headers](#get-response-headers-1)\n  - [Download file with `responseType: 'stream' | 'blob'`](#download-file-with-responsetype-stream--blob)\n  - [Use stream](#use-stream)\n- [Migrate from `fetch` to **xior**](#migrate-from-fetch-to-xior)\n  - [GET](#get-1)\n  - [POST](#post-1)\n  - [Abort a fetch](#abort-a-fetch)\n  - [Sending a request with credentials included](#sending-a-request-with-credentials-included)\n  - [Uploading a file](#uploading-a-file)\n  - [Processing a text file line by line](#processing-a-text-file-line-by-line)\n- [API Reference](#api-reference)\n- [Star History](#star-history)\n- [Thanks](#thanks)\n\n## Getting Started\n\n### Installing\n\n#### Package manager\n\n```sh\n# npm\nnpm install xior\n\n# pnpm\npnpm add xior\n\n# bun\nbun add xior\n\n# yarn\nyarn add xior\n```\n\n#### Use CDN\n\n\u003e Since v0.2.1, xior supports UMD format\n\nUse jsDelivr CDN:\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  xior.get('https://exmapledomain.com/api').then((res) =\u003e {\n    console.log(res.data);\n  });\n\u003c/script\u003e\n```\n\nUse unpkg CDN:\n\n```html\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  xior.get('https://exmapledomain.com/api').then((res) =\u003e {\n    console.log(res.data);\n  });\n\u003c/script\u003e\n```\n\n### Create instance\n\n```ts\nimport xior from 'xior';\n\nexport const xiorInstance = xior.create({\n  baseURL: 'https://apiexampledomain.com/api',\n  headers: {\n    // put your common custom headers here\n  },\n});\n```\n\n### GET / POST / DELETE / PUT / PATCH / OPTIONS / HEAD\n\nGET\n\n\u003e `HEAD` / `DELETE` / `OPTIONS` are same usage with `GET` method\n\n```ts\nasync function run() {\n  const { data } = await xiorInstance.get('/');\n\n  // with params and support nested params\n  const { data: data2 } = await xiorInstance.get('/', { params: { a: 1, b: 2, c: { d: 1 } } });\n\n  // with headers\n  const { data: data3 } = await xiorInstance.get('/', {\n    params: { a: 1, b: 2 },\n    headers: {\n      'content-type': 'application/x-www-form-urlencoded',\n    },\n  });\n\n  // types\n  const { data: data4 } = await xiorInstance.get\u003c{ field1: string; field2: number }\u003e('/');\n}\n```\n\nPOST\n\n\u003e `PUT`/`PATCH` methods are same usage with `POST`\n\n```ts\nasync function run() {\n  const { data: data3 } = await xiorInstance.post\u003c{ field1: string; field2: number }\u003e(\n    '/',\n    { a: 1, b: '2' },\n    {\n      params: { id: 1 },\n      headers: {},\n    }\n  );\n}\n```\n\n### Change default headers or params\n\n```ts\nimport xior from 'xior';\n\nexport const xiorInstance = xior.create({\n  baseURL: 'https://apiexampledomain.com/api',\n});\n\nfunction setAccessToken(token: string) {\n  // xiorInstance.defaults.params['x'] = 1;\n  xiorInstance.defaults.headers['Authorization'] = `Bearer ${token}`;\n}\n\nfunction removeUserToken() {\n  // delete xiorInstance.defaults.params['x'];\n  delete xiorInstance.defaults.headers['Authorization'];\n}\n```\n\n### Get response headers\n\n```ts\nimport xior from 'xior';\n\nconst xiorInstance = xior.create({\n  baseURL: 'https://apiexampledomain.com/api',\n});\n\nconst { data, headers } = await xiorInstance.get('/');\n\nconsole.log(headers.get('X-Header-Name'));\n```\n\n### Upload file\n\n**xior** supports file uploads using the `FormData` API and provides an optional `'xior/plugins/progress'` plugin for simulating upload progress, usage similar to Axios.\n\n```ts\nimport Xior from 'xior';\nimport uploadDownloadProgressPlugin from 'xior/plugins/progress';\n\nconst http = Xior.create({});\n\nhttp.plugins.use(\n  uploadDownloadProgressPlugin({\n    progressDuration: 5 * 1000,\n  })\n);\n\nconst formData = FormData();\nformData.append('file', fileObject);\nformData.append('field1', 'val1');\nformData.append('field2', 'val2');\n\nhttp.post('/upload', formData, {\n  onUploadProgress(e) {\n    console.log(`Upload progress: ${e.progress}%`);\n  },\n  // progressDuration: 10 * 1000\n});\n```\n\n### Using interceptors\n\n**xior** supports interceptors similar to Axios, allowing you to modify requests and handle responses programmatically.\n\nRequest interceptors:\n\n```ts\nimport xior, { merge } from 'xior';\n\nconst http = xior.create({\n  // ...options\n});\n\nhttp.interceptors.request.use((config) =\u003e {\n  const token = localStorage.getItem('REQUEST_TOKEN');\n  if (!token) return config;\n\n  return merge(config, {\n    headers: {\n      Authorization: `Bearer ${token}`,\n    },\n  });\n});\n\n// One more interceptors for request\nhttp.interceptors.request.use((config) =\u003e {\n  return config;\n});\n\nasync function getData() {\n  const { data } = await http.get('/');\n  console.log(data);\n  return data;\n}\n```\n\nResponse interceptors:\n\n```ts\nimport xior, { merge } from 'xior';\n\nconst http = xior.create({});\n\nhttp.interceptors.response.use(\n  (result) =\u003e {\n    const { data, request: config, response: originalResponse } = result;\n    return result;\n  },\n  async (error) =\u003e {\n    if (error instanceof TypeError) {\n      console.log(`Request error:`, error);\n    }\n    if (error?.response?.status === 401) {\n      localStorage.removeItem('REQUEST_TOKEN');\n    }\n    return Promise.reject(error);\n  }\n);\n\nasync function getData() {\n  const { data } = await http.get('/');\n  console.log(data);\n  return data;\n}\n```\n\n### Cleanup interceptors\n\n```ts\nimport xior from 'xior';\n\nconst http = xior.create({});\n\n// Cleanup request interceptors\nconst handler1 = http.interceptors.request.use((config) =\u003e {\n  return config;\n});\nhttp.interceptors.request.eject(handler1);\n// Cleanup all request interceptors\n// http.interceptors.request.clear()\n\n// Cleanup response interceptors\nconst handler2 = http.interceptors.response.use((res) =\u003e {\n  return res;\n});\nhttp.interceptors.response.eject(handler2);\n// Cleanup all response interceptors\n// http.interceptors.response.clear()\n```\n\n### Timeout and Cancel request\n\nTimeout:\n\n```ts\nimport xior from 'xior';\n\nconst instance = xior.create({\n  timeout: 120 * 1000, // set default timeout\n});\n\nawait instance.post(\n  'http://httpbin.org',\n  {\n    a: 1,\n    b: 2,\n  },\n  {\n    timeout: 60 * 1000, // override default timeout 120 * 1000\n  }\n);\n```\n\nCancel request:\n\n```ts\nimport xior from 'xior';\nconst instance = xior.create();\n\nconst controller = new AbortController();\n\nxiorInstance.get('http://httpbin.org', { signal: controller.signal }).then((res) =\u003e {\n  console.log(res.data);\n});\n\nclass CancelRequestError extends Error {}\ncontroller.abort(new CancelRequestError()); // abort request with custom error\n```\n\n### Proxy or use custom fetch implementations\n\nSee [3. How can I use custom fetch implementation or How to support **proxy** feature?](#3-how-can-i-use-custom-fetch-implementation-or-how-to-support-proxy-feature)\n\n### Custom data parser\n\nIn `xior`, the default response parser is this:\n\n```ts\nlet data = response.text();\nif (data) {\n  try {\n    data = JSON.parse(data);\n  } catch (e) {}\n}\nreturn data;\n```\n\nBut maybe we don't want to do it this way; instead, we want to parse the data based on the `content-type` from `response`'s headers. So, we can do it this way:\n\n```ts\nimport axios from 'xior';\n\nconst http = Xior.create({\n  baseURL,\n  responseType: 'custom', // Tell xior no need to parse body\n});\n\n// Define content type matchers as [responseMethod, [regexpPatterns]]\nconst typeMatchers = [\n  ['json', [/^application\\/.*json$/, /^$/]],\n  ['text', [/^text\\//, /^image\\/svg\\+xml$/, /^application\\/.*xml$/]],\n  // ['arrayBuffer', [/^application\\/octet-stream/]],\n] as const;\n\nhttp.interceptors.response.use(\n  async (res) =\u003e {\n    try {\n      if (res.config.responseType !== 'custom') return res;\n\n      const { response } = res;\n      const headers = response?.headers;\n      if (!response || headers.get('Content-Length') === '0') return res;\n\n      const contentType = headers.get('Content-Type')?.split(';')?.[0]?.trim() || '';\n\n      // Find matching response method using the typeMatchers array\n      const matchedType = typeMatchers.find(([_, patterns]) =\u003e\n        patterns.some((pattern) =\u003e pattern.test(contentType))\n      );\n\n      if (matchedType) {\n        const [method] = matchedType;\n        res.data = await response[method]();\n      } else {\n        console.warn(`Unknown Content-Type: ${contentType}`);\n      }\n\n      return res;\n    } catch (error) {\n      console.error('Interceptor error:', error);\n      return Promise.reject(error);\n    }\n  },\n  (error) =\u003e Promise.reject(error)\n);\n```\n\n### Encrypt and Decrypt Example\n\nWe can use interceptors easily to handle encrypt/decrypt.\n\nCreate `encryption.ts`:\n\n```ts\n// encryption.ts\nexport const SECRET = '\u0026*\u0026*^SDxsdasdas776';\n\nexport function encrypt(data: string) {\n  return data + '____' + SECRET;\n}\n\nexport function decrypt(data: string, s?: string) {\n  return data.replace('____' + (s || SECRET), '');\n}\n```\n\nCreate `xior-instance.ts`:\n\n```ts\nimport xior from 'xior';\n\nimport { SECRET, encrypt, decrypt } from './encryption';\n\nexport const instance = xior.create();\n\ninstance.interceptors.request.use((req) =\u003e {\n  req.headers['X'] = SECRET;\n\n  if (req.url \u0026\u0026 req.data) {\n    const result = JSON.stringify(req.data);\n    const blob = encrypt(result);\n    req.data = { blob };\n  }\n\n  return req;\n});\n\ninstance.interceptors.response.use((res) =\u003e {\n  if (res.request.url \u0026\u0026 res.data?.blob) {\n    res.data = decrypt(res.data.blob);\n    try {\n      res.data = JSON.parse(res.data);\n    } catch (e) {\n      console.error(e);\n    }\n  }\n  return res;\n});\n```\n\n\u003e Check test code in `tests/src/tests/encrypt-decrypt/`\n\n### Tips: Make your SSR(Server-side Rendering) app more stable and faster\n\n**How do we achieve this?** By using Xior's plugins:\n\n1. If a `GET` request fails, allow retries for a second chance at success.\n2. If retries still fail, return cached data (if available) to prevent page crashes or error pages.\n3. Deduplicate `GET` requests to avoid redundant calls.\n4. Throttle `GET` requests to control request frequency.\n5. For large data that isn’t needed in real-time (like i18n JSON files), serve cached data first and fetch updates in the background.\n   Example code:\n\n```ts\nimport xior, { XiorError as AxiosError } from 'xior';\nimport errorRetryPlugin from 'xior/plugins/error-retry';\nimport dedupePlugin from 'xior/plugins/dedupe';\nimport throttlePlugin from 'xior/plugins/throttle';\nimport errorCachePlugin from 'xior/plugins/error-cache';\n\n// Setup\nconst http = axios.create({\n  baseURL: 'http://localhost:3000',\n});\nhttp.plugins.use(errorRetryPlugin());\nhttp.plugins.use(errorCachePlugin());\nhttp.plugins.use(dedupePlugin()); // Prevent same GET requests from occurring simultaneously.\nhttp.plugins.use(throttlePlugin()); // Throttle same `GET` request in 1000ms\n\n// 1. If `GET` data error, at least have chance to retry;\n// 2. If retry still error, return the cache data(if have) to prevent page crash or show error page;\nconst res = await http.get('/api/get-data'); // these will retry if have error\nif (res.fromCache) {\n  console.log(`the data from cahce`, res.cacheTime);\n}\n\n// 3. Dedupe the same `GET` requests, this will only sent 1 real request\nawait Promise.all([\n  http.get('/api/get-data-2'),\n  http.get('/api/get-data-2'),\n  http.get('/api/get-data-2'),\n]);\n\n// 4. Throttle the `GET` requests,\n//    we want throttle some larget data request in 10s, default is 1s\nhttp.get('/api/get-some-big-data', { threshold: 10e3 });\n\n// 5. If have cache data, return the cache data first,\n//    and run the real request in background\nhttp.get('/api/get-some-big-data', { threshold: 10e3, useCacheFirst: true });\n```\n\n## Plugins\n\n**xior** offers a variety of built-in plugins to enhance its functionality:\n\n- [Error retry plugin](#error-retry-plugin)\n- [Request dedupe plugin](#request-dedupe-plugin)\n- [Request throttle plugin](#request-throttle-plugin)\n- [Error cache plugin](#error-cache-plugin)\n- [Cache plugin](#cache-plugin)\n- [Upload and download progress plugin](#upload-and-download-progress-plugin)\n- [Mock plugin](#Mock-plugin)\n- [Auth refresh token plugin(from community)](#auth-refresh-token-pluginfrom-community)\n- [Auth refresh token plugin(built-in)](#auth-refresh-token-pluginbuilt-in)\n\nUsage:\n\n```ts\nimport xior from 'xior';\nimport errorRetryPlugin from 'xior/plugins/error-retry';\nimport throttlePlugin from 'xior/plugins/throttle';\nimport cachePlugin from 'xior/plugins/cache';\nimport uploadDownloadProgressPlugin from 'xior/plugins/progress';\n\nconst http = xior.create();\n\nhttp.plugins.use(errorRetryPlugin());\nhttp.plugins.use(throttlePlugin());\nhttp.plugins.use(cachePlugin());\nhttp.plugins.use(uploadDownloadProgressPlugin());\n```\n\n### Error retry plugin\n\n\u003e Retry the failed request with special times\n\nAPI:\n\n```ts\nfunction errorRetryPlugin(options: {\n  retryTimes?: number;\n  retryInterval?: number | ((errorCount: number) =\u003e number);\n  enableRetry?: boolean | (config: XiorRequestConfig, error: XiorError | Error) =\u003e boolean | undefined;\n  onRetry?: (config: XiorRequestConfig, error: XiorError | Error, count: number) =\u003e void;\n}): XiorPlugin;\n```\n\nThe `options` object:\n\n| Param         | Type                                                                                        | Default value                                                | Description                                                                                                                                        |\n| ------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |\n| retryTimes    | number                                                                                      | 2                                                            | Set the retry times for failed request                                                                                                             |\n| retryInterval | number \\| ((errorCount: number, config: XiorRequestConfig, error: XiorError) =\u003e number)     | 3000                                                         | After first time retry, the next retries interval time, default interval is 3 seconds; you can use function as param to return interval number too |\n| enableRetry   | boolean \\| ((config: XiorRequestConfig, error: XiorError \\| Error) =\u003e boolean \\| undefined) | (config, error) =\u003e config.method === 'GET' \\|\\| config.isGet | Default only retry if `GET` request error and `retryTimes \u003e 0`                                                                                     |\n| onRetry       | boolean \\| ((config: XiorRequestConfig, error: XiorError \\| Error, count: number) =\u003e void)  | undefined                                                    | For log retry info                                                                                                                                 |\n\nBasic usage:\n\n```ts\nimport xior from 'xior';\nimport errorRetryPlugin from 'xior/plugins/error-retry';\n\nconst http = xior.create();\nhttp.plugins.use(\n  errorRetryPlugin({\n    retryTimes: 3,\n    // retryInterval: 3000,\n    retryInterval(count, config, error) {\n      // if (error.response?.status === 500) return 10e3;\n      return count * 1e3;\n    },\n    onRetry(config, error, count) {\n      console.log(`${config.method} ${config.url} retry ${count} times`);\n    },\n    // enableRetry(config, error) {\n    //   if ([401, 400].includes(error.response?.status)) { // no retry when status is 400 or 401\n    //     return false;\n    //   }\n    //   // no return or return `undefined` here, will reuse the default `enableRetry` logic\n    // },\n  })\n);\n\n// if request error, max retry 3 times until success\nhttp.get('/api1');\n\n// if request error, will not retry, because `retryTimes: 0`\nhttp.get('/api2', { retryTimes: 0 });\n\n// if POST request error, will not retry\nhttp.post('/api1');\n\n// Use `enableRetry: true` to support post method, max retry 5 times until success\nhttp.post('/api1', null, { retryTimes: 5, enableRetry: true });\n```\n\nAdvance usage:\n\n\u003e The retry key for the unique request generated by use `params` and `data`, if your request depends on `headers`, you can add request interceptor to add headers's value to `params`:\n\n```ts\nimport xior from 'xior';\nimport errorRetryPlugin from 'xior/plugins/error-retry';\n\nconst http = xior.create();\nhttp.plugins.use(errorRetryPlugin());\n\nhttp.interceptors.request.use((config) =\u003e {\n  config.params['___k'] = `${config.headers['x-custom-field'] || ''}`;\n  return config;\n});\n```\n\nUse CDN:\n\nUsing jsDelivr CDN:\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/error-retry.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  xior.plugins.use(xiorErrorRetry());\n\u003c/script\u003e\n```\n\nUsing unpkg CDN:\n\n```html\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/plugins/error-retry.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  xior.plugins.use(xiorErrorRetry());\n\u003c/script\u003e\n```\n\n### Request throttle plugin\n\n\u003e Throttle GET requests(or custom) most once per threshold milliseconds, filter repeat requests in certain time.\n\nAPI:\n\n```ts\nfunction throttleRequestPlugin(options: {\n  /** threshold in milliseconds, default: 1000ms */\n  threshold?: number;\n  /**\n   * check if we need enable throttle, default only `GET` method or`isGet: true` enable\n   */\n  enableThrottle?: boolean | ((config?: XiorRequestConfig) =\u003e boolean | undefined);\n  throttleCache?: ICacheLike\u003cRecordedCache\u003e;\n  onThrottle?: (config: XiorRequestConfig) =\u003e void;\n  throttleItems?: number;\n}): XiorPlugin;\n```\n\nThe `options` object:\n\n\u003e You can override default value in each request's own config (Except `throttleCache`)\n\n| Param          | Type                                                             | Default value                                         | Description                                                                                |\n| -------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |\n| threshold      | number                                                           | 1000                                                  | The number of milliseconds to throttle request invocations to                              |\n| enableThrottle | boolean \\| ((config: XiorRequestConfig) =\u003e boolean \\| undefined) | (config) =\u003e config.method === 'GET' \\|\\| config.isGet | Default only enabled in `GET` request                                                      |\n| throttleCache  | CacheLike                                                        | lru(100)                                              | CacheLike instance that will be used for storing throttled requests, use `tiny-lru` module |\n| throttleItems  | number                                                           | 100                                                   | The max number of throttle items in the default LRU cache                                  |\n\nBasic usage:\n\n```ts\nimport xior from 'xior';\nimport throttlePlugin from 'xior/plugins/throttle';\n\nconst http = xior.create();\nhttp.plugins.use(\n  throttlePlugin({\n    onThrottle(config) {\n      console.log(`Throttle requests ${config.method} ${config.url}`);\n    },\n  })\n);\n\nhttp.get('/'); // make real http request\nhttp.get('/'); // response from cache\nhttp.get('/'); // response from cache\nhttp.get('/', { throttle: 2e3 }); // custom throttle to 2 seconds\n\nhttp.post('/'); // make real http request\nhttp.post('/'); // make real http request\nhttp.post('/'); // make real http request\n\nhttp.post('/', null, {\n  enableThrottle: true,\n}); // make real http request\nhttp.post('/', null, {\n  enableThrottle: true,\n}); // response from cache\nhttp.post('/', null, {\n  enableThrottle: true,\n}); // response from cache\n\n// make post method as get method use `{isGet: true}`,\n// useful when some API is get data but the method is `post`\nhttp.post('/get', null, {\n  isGet: true,\n}); // make real http request\nhttp.post('/get', null, {\n  isGet: true,\n}); // response from cache\nhttp.post('/get', null, {\n  isGet: true,\n}); // response from cache\n```\n\nUse CDN:\n\nUsing jsDelivr CDN:\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/throttle.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  xior.plugins.use(xiorThrottle());\n\u003c/script\u003e\n```\n\nUsing unpkg CDN:\n\n```html\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/plugins/throttle.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  xior.plugins.use(xiorThrottle());\n\u003c/script\u003e\n```\n\n### Request dedupe plugin\n\n\u003e Prevents having multiple identical requests on the fly at the same time.\n\nAPI:\n\n```ts\nfunction dedupeRequestPlugin(options: {\n  /**\n   * check if we need enable dedupe, default only `GET` method or`isGet: true` enable\n   */\n  enableDedupe?: boolean | ((config?: XiorRequestConfig) =\u003e boolean);\n  onDedupe?: (config: XiorRequestConfig) =\u003e void;\n}): XiorPlugin;\n```\n\nBasic usage:\n\n```ts\nimport xior from 'xior';\nimport dedupePlugin from 'xior/plugins/dedupe';\n\nconst http = xior.create();\nhttp.plugins.use(\n  dedupePlugin({\n    onDedupe(config) {\n      console.log(`Dedupe ${config.method} ${config.url}`);\n    },\n  })\n);\n\nhttp.get('/'); // make real http request\nhttp.get('/'); // response from previous if previous request return response\nhttp.get('/'); // response from previous if previous request return response\n\nhttp.post('/'); // make real http request\nhttp.post('/'); // make real http request\nhttp.post('/'); // make real http request\n```\n\nUse CDN:\n\nUsing jsDelivr CDN:\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/dedupe.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  xior.plugins.use(xiorDedupe());\n\u003c/script\u003e\n```\n\nUsing unpkg CDN:\n\n```html\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/plugins/dedupe.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  xior.plugins.use(xiorDedupe());\n\u003c/script\u003e\n```\n\n### Error cache plugin\n\n\u003e When request error, if have cached data then use the cached data\n\nAPI:\n\n```ts\nfunction errorCachePlugin(options: {\n  enableCache?: boolean | ((config?: XiorRequestConfig) =\u003e boolean | undefined);\n  defaultCache?: ICacheLike\u003cXiorPromise\u003e;\n  useCacheFirst?: boolean;\n}): XiorPlugin;\n```\n\nThe `options` object:\n\n| Param         | Type                                                             | Default value                                         | Description                                                                                                                                                                                                                     |\n| ------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| enableCache   | boolean \\| ((config: XiorRequestConfig) =\u003e boolean \\| undefined) | (config) =\u003e config.method === 'GET' \\|\\| config.isGet | Default only enabled in `GET` request                                                                                                                                                                                           |\n| defaultCache  | CacheLike                                                        | lru(100, 0)                                           | will used for storing requests by default, except you define a custom Cache with your request config, use `tiny-lru` module                                                                                                     |\n| useCacheFirst | boolean                                                          | false                                                 | If `useCacheFirst: true` and there's a cache, it will return the cached response first, then run fetching task on the background. This is useful when the response takes a long time, and the data is unnecessary in real-time. |\n| cacheItems    | number                                                           | 100                                                   | The max number of error cache items in the default LRU cache                                                                                                                                                                    |\n\nBasic usage:\n\n```ts\nimport xior from 'xior';\nimport errorCachePlugin from 'xior/plugins/error-cache';\n\nconst http = xior.create();\nhttp.plugins.use(errorCachePlugin({}));\n\nhttp.get('/users'); // make real http request, and cache the response\nconst res = await http.get('/users'); // if request error, use the cache data\nif (res.fromCache) {\n  // if `fromCache` is true, means data from cache!\n  console.log('data from cache!');\n  console.log('data cache timestamp: ', res.cacheTime);\n  // and get what's the error\n  console.log('error', res.error);\n}\n\nhttp.post('/users'); // no cache for post\n\nhttp.post('/users', { isGet: true }); // but with `isGet: true` can let plugins know this is `GET` behavior! then will cache data\n```\n\nUse CDN:\n\nUsing jsDelivr CDN:\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/error-cache.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  xior.plugins.use(xiorErrorCache());\n\u003c/script\u003e\n```\n\nUsing unpkg CDN:\n\n```html\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/plugins/error-cache.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  xior.plugins.use(xiorErrorCache());\n\u003c/script\u003e\n```\n\n### Cache plugin\n\n\u003e Makes xior cacheable\n\n\u003e Good to Know: Next.js already support cache for fetch in server side. [More detail](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server-with-fetch)\n\n\u003e Different with `error-cache` plugin: this plugin will use the data in cache if the cache data not expired.\n\nAPI:\n\n```ts\nfunction cachePlugin(options: {\n  enableCache?: boolean | ((config?: XiorRequestConfig) =\u003e boolean);\n  defaultCache?: ICacheLike\u003cXiorPromise\u003e;\n  cacheItems?: number;\n}): XiorPlugin;\n```\n\nThe `options` object:\n\n| Param        | Type                                                             | Default value                                         | Description                                                                                                                 |\n| ------------ | ---------------------------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |\n| enableCache  | boolean \\| ((config: XiorRequestConfig) =\u003e boolean \\| undefined) | (config) =\u003e config.method === 'GET' \\|\\| config.isGet | Default only enabled in `GET` request                                                                                       |\n| defaultCache | CacheLike                                                        | lru(100, 1000\\*60\\*5)                                 | will used for storing requests by default, except you define a custom Cache with your request config, use `tiny-lru` module |\n| cacheItems   | number                                                           | 100                                                   | Custom the default LRU cache numbers                                                                                        |\n| cacheTime    | number                                                           | 1000 \\* 60 \\* 5                                       | Custom the default LRU cache time                                                                                           |\n\nBasic usage:\n\n```ts\nimport xior from 'xior';\nimport cachePlugin from 'xior/plugins/cache';\n\nconst http = xior.create();\nhttp.plugins.use(\n  cachePlugin({\n    cacheItems: 100,\n    cacheTime: 1e3 * 60 * 5,\n  })\n);\n\nhttp.get('/users'); // make real http request\nhttp.get('/users'); // get cache from previous request\nhttp.get('/users', { enableCache: false }); // disable cache manually and the real http request\n\nhttp.post('/users'); // default no cache for post\n\n// enable cache manually in post request\nhttp.post('/users', { enableCache: true }); // make real http request\nconst res = await http.post('/users', { enableCache: true }); // get cache from previous request\nif (res.fromCache) {\n  // if `fromCache` is true, means data from cache!\n  console.log('data from cache!', res.cacheKey, res.cacheTime);\n}\n```\n\nAdvanced:\n\n```ts\nimport xior from 'xior';\nimport cachePlugin from 'xior/plugins/cache';\nimport { lru } from 'tiny-lru';\n\nconst http = xior.create({\n  baseURL: 'https://example-domain.com/api',\n  headers: { 'Cache-Control': 'no-cache' },\n});\nhttp.plugins.use(\n  cachePlugin({\n    // disable the default cache\n    enableCache: false,\n    cacheItems: 1000,\n    cacheTime: 1e3 * 60 * 10,\n  })\n);\n\nhttp.get('/users', { enableCache: true }); // manually enable cache for this request\nhttp.get('/users', { enableCache: true }); // get cache from  previous request\n\nconst cacheA = lru(100);\n// a actual request made and cached due to force update configured\nhttp.get('/users', { enableCache: true, defaultCache: cacheA, forceUpdate: true });\n```\n\n### Persist cache data\n\nHow to persist cache data to the filesystem to prevent loss after a server restart?\n\nFor more details, refer to this GitHub issue: [GitHub issue 33](https://github.com/suhaotian/xior/issues/33)\n\n### Upload and download progress plugin\n\n\u003e Enable upload and download progress like axios, but the progress is simulated,\n\u003e This means it doesn't represent the actual progress but offers a user experience similar to libraries like axios.\n\nAPI:\n\n```ts\nfunction progressPlugin(options: {\n  /** default: 5*1000 ms */\n  progressDuration?: number;\n}): XiorPlugin;\n```\n\nThe `options` object:\n\n| Param            | Type   | Default value | Description                                          |\n| ---------------- | ------ | ------------- | ---------------------------------------------------- |\n| progressDuration | number | 5000          | The upload or download progress grow to 99% duration |\n\nBasic usage:\n\n```ts\nimport xior from 'xior';\nimport uploadDownloadProgressPlugin from 'xior/plugins/progress';\n\nconst http = xior.create({});\nhttp.plugins.use(uploadDownloadProgressPlugin());\n\nconst formData = FormData();\nformData.append('file', fileObject);\nformData.append('field1', 'val1');\nformData.append('field2', 'val2');\n\nhttp.post('/upload', formData, {\n  // simulate upload progress to 99% in 10 seconds, default is 5 seconds\n  progressDuration: 10 * 1000,\n  onUploadProgress(e) {\n    console.log(`Upload progress: ${e.progress}%`);\n  },\n  // onDownloadProgress(e) {\n  //   console.log(`Download progress: ${e.progress}%`);\n  // },\n});\n```\n\nUse CDN:\n\nUsing jsDelivr CDN:\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/progress.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  xior.plugins.use(xiorProgress());\n\u003c/script\u003e\n```\n\nUsing unpkg CDN:\n\n```html\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/plugins/progress.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  xior.plugins.use(xiorProgress());\n\u003c/script\u003e\n```\n\n### Mock plugin\n\n\u003e This plugin let you eaisly mock requests\n\nUsage:\n\nwith `GET`:\n\n```ts\nimport xior from 'xior';\nimport MockPlugin from 'xior/plugins/mock';\n\nconst instance = xior.create();\nconst mock = new MockPlugin(instance);\n\n// Mock any GET request to /users\n// arguments for reply are (status, data, headers)\nmock.onGet('/users').reply(\n  200,\n  {\n    users: [{ id: 1, name: 'John Smith' }],\n  },\n  {\n    'X-Custom-Response-Header': '123',\n  }\n);\n\ninstance.get('/users').then(function (response) {\n  console.log(response.data);\n  console.log(response.headers.get('X-Custom-Response-Header')); // 123\n});\n\n// Mock GET request to /users when param `searchText` is 'John'\n// arguments for reply are (status, data, headers)\nmock.onGet('/users', { params: { searchText: 'John' } }).reply(200, {\n  users: [{ id: 1, name: 'John Smith' }],\n});\n\ninstance.get('/users', { params: { searchText: 'John' } }).then(function (response) {\n  console.log(response.data);\n});\n```\n\nwith `POST`:\n\n```ts\nimport xior from 'xior';\nimport MockPlugin from 'xior/plugins/mock';\n\nconst instance = xior.create();\nconst mock = new MockPlugin(instance);\n\n// Mock any POST request to /users\n// arguments for reply are (status, data, headers)\nmock.onPost('/users').reply(\n  200,\n  {\n    users: [{ id: 1, name: 'John Smith' }],\n  },\n  {\n    'X-Custom-Response-Header': '123',\n  }\n);\n\ninstance.post('/users').then(function (response) {\n  console.log(response.data);\n  console.log(response.headers.get('X-Custom-Response-Header')); // 123\n});\n\n// Mock POST request to /users when param `searchText` is 'John'\n// arguments for reply are (status, data, headers)\nmock.onPost('/users', null, { params: { searchText: 'John' } }).reply(200, {\n  users: [{ id: 1, name: 'John Smith' }],\n});\n\ninstance.get('/users', null, { params: { searchText: 'John' } }).then(function (response) {\n  console.log(response.data);\n});\n\n// Mock POST request to /users when body `searchText` is 'John'\n// arguments for reply are (status, data, headers)\nmock.onPost('/users', { searchText: 'John' }).reply(200, {\n  users: [{ id: 1, name: 'John Smith' }],\n});\n\ninstance.get('/users', { searchText: 'John' }).then(function (response) {\n  console.log(response.data);\n});\n```\n\n**More details**, [check here](./Mock-plugin.md).\n\nUse CDN:\n\nUsing jsDelivr CDN:\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/xior@0.7.8/plugins/mock.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  const mock = new xiorMock(xior);\n\u003c/script\u003e\n```\n\nUsing unpkg CDN:\n\n```html\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/dist/xior.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Load plugin --\u003e\n\u003cscript src=\"https://unpkg.com/xior@0.7.8/plugins/mock.umd.js\"\u003e\u003c/script\u003e\n\n\u003c!-- Usage --\u003e\n\u003cscript\u003e\n  console.log(xior.VERSION);\n\n  const mock = new xiorMock(xior);\n\u003c/script\u003e\n```\n\n### Auth refresh token plugin(from community)\n\nWe will use `xior-auth-refresh` plugin from the community: https://github.com/Audiu/xior-auth-refresh\n\n**Install:**\n\n```sh\nnpm install xior-auth-refresh --save\n# or\nyarn add xior-auth-refresh\n# or\npnpm add xior-auth-refresh\n```\n\n**Usage:**\n\n```ts\nimport xior from 'xior';\nimport createAuthRefreshInterceptor from 'xior-auth-refresh';\n\n// Function that will be called to refresh authorization\nconst refreshAuthLogic = (failedRequest) =\u003e\n  xior.post('https://www.example.com/auth/token/refresh').then((tokenRefreshResponse) =\u003e {\n    localStorage.setItem('token', tokenRefreshResponse.data.token);\n    failedRequest.response.config.headers['Authorization'] =\n      'Bearer ' + tokenRefreshResponse.data.token;\n    return Promise.resolve();\n  });\n\n// Instantiate the interceptor\ncreateAuthRefreshInterceptor(xior, refreshAuthLogic);\n\n// Make a call. If it returns a 401 error, the refreshAuthLogic will be run,\n// and the request retried with the new token\nxior.get('https://www.example.com/restricted/area').then(/* ... */).catch(/* ... */);\n```\n\nMore: https://github.com/Audiu/xior-auth-refresh\n\n### Auth refresh token plugin(built-in)\n\nUsage:\n\n```ts\nimport xior, { XiorResponse } from 'xior';\nimport errorRetry from 'xior/plugins/error-retry';\nimport setupTokenRefresh from 'xior/plugins/token-refresh';\n\nconst instance = xior.create();\n\nconst TOKEN_KEY = 'TOKEN';\nfunction getToken() {\n  return localStorage.getItem(TOKEN_KEY);\n}\nfunction setToken(token: string) {\n  return localStorage.setItem(TOKEN_KEY, token);\n}\nfunction deleteToken() {\n  return localStorage.getItem(TOKEN_KEY);\n}\n\ninstance.interceptors.request.use((config) =\u003e {\n  const token = getToken();\n  if (token) {\n    config.headers['Authorization'] = `Bearer ${token}`;\n  }\n  return config;\n});\n\nfunction shouldRefresh(response: XiorResponse) {\n  const token = getToken();\n  return Boolean(token \u0026\u0026 response?.status \u0026\u0026 [401, 403].includes(response.status));\n}\ninstance.plugins.use(\n  errorRetry({\n    enableRetry: (config, error) =\u003e {\n      if (error?.response \u0026\u0026 shouldRefresh(error.response)) {\n        return true;\n      }\n      // return false\n    },\n  })\n);\nsetupTokenRefresh(http, {\n  shouldRefresh,\n  async refreshToken(error) {\n    try {\n      const { data } = await http.post('/token/new');\n      if (data.token) {\n        setToken(data.token);\n      } else {\n        throw error;\n      }\n    } catch (e) {\n      // something wrong, delete old token\n      deleteToken();\n      return Promise.reject(error);\n    }\n  },\n});\n```\n\n### Create your own custom plugin\n\n**xior** let you easily to create custom plugins.\n\nHere are examples:\n\n1. Simple Logging plugin:\n\n```ts\nimport xior from 'xior';\n\nconst instance = xior.create();\ninstance.plugins.use(function logPlugin(adapter, instance) {\n  return async (config) =\u003e {\n    const start = Date.now();\n    const res = await adapter(config);\n    console.log('%s %s %s take %sms', config.method, config.url, res.status, Date.now() - start);\n    return res;\n  };\n});\n```\n\n2. Check built-in plugins get more inspiration:\n\nCheck [src/plugins](./src/plugins)\n\n### Cleanup plugins example\n\n```ts\nimport xior from 'xior';\nimport errorRetryPlugin from 'xior/plugins/error-retry';\nconst http = xior.create();\n\nconst pluginHandler = http.plugins.use(errorRetryPlugin());\nhttp.plugins.eject(pluginHandler);\n\n// Cleanup all plugins\n// http.plugins.clear()\n```\n\n## Helper functions\n\n**xior** has built-in helper functions, may useful for you:\n\n```ts\nimport lru from 'tiny-lru';\nimport {\n  encodeParams,\n  merge as deepMerge,\n  delay as sleep,\n  buildSortedURL,\n  isAbsoluteURL,\n  joinPath,\n  isXiorError,\n  trimUndefined,\n  Xior,\n} from 'xior';\n```\n\n## FAQ\n\n**xior** frequently asked questions.\n\n### 1. Is **xior** 100% compatiable with `axios`?\n\n**No**, but **xior** offers a similar API like axios: `axios.create` / `axios.interceptors` / `.get/post/put/patch/delete/head/options`.\n\n**The most common change is replacing `axios` with `xior` and checking if the TypeScript types pass**:\n\n```ts\nimport axios, {\n  XiorError as AxiosError,\n  isXiorError as isAxiosError,\n  XiorRequestConfig as AxiosRequestConfig,\n  XiorResponse as AxiosResponse,\n} from 'xior';\n\nconst instance = axios.create({\n  baseURL: '...',\n  timeout: 20e3,\n});\n```\n\n### 2. Can I use xior in projects like Bun, Expo, React Native, RemixJS, Next.js, Vue, Nuxt.js, Tauri or `NervJS/Taro`?\n\n**Yes**, **xior** works anywhere where the native `fetch` API is supported.\nEven if the environment doesn't support `fetch`, you can use a `fetch` polyfill like for older browsers.\n\nFor `Tauri` or `Taro`: check [3. How can I use custom fetch implementation or How to support **proxy** feature?](https://github.com/suhaotian/xior?tab=readme-ov-file#3-how-can-i-use-custom-fetch-implementation-or-how-to-support-proxy-feature)\n\n### 3. How can I use custom fetch implementation or How to support **proxy** feature?\n\nTo support **proxy** feature or custom fetch implementation, we can use `node-fetch`, nodejs `undici`, or `@tauri-apps/plugin-http` module's fetch implementation to replace the built-in `fetch`.\n\nFor example **undici**:\n\n```sh\nnpm install undici\n```\n\n```ts\nimport { fetch as undiciFetch, FormData, Agent, type RequestInit as RequestInit_ } from 'undici';\n\n/** For TypeScript types **/\ndeclare global {\n  interface RequestInit extends RequestInit_ {}\n}\n\n/** Create Agent **/\nconst agent = new Agent({\n  connections: 10,\n});\n\nconst xiorInstance = xior.create({\n  baseURL: 'https://example.com',\n  fetch: undiciFetch,\n  dispatcher: agent,\n});\n```\n\nFor example **node-fetch**:\n\n```sh\n# For ESM module\nnpm install node-fetch\n\n# For CommonJS module\n# npm install node-fetch@v2.7.0\n# npm install @types/node-fetch -D\n```\n\n```ts\nimport nodeFetch, { RequestInit as RequestInit_ } from 'node-fetch';\nimport http from 'node:http';\nimport https from 'node:https';\n\n/** For TypeScript types **/\ndeclare global {\n  interface RequestInit extends RequestInit_ {}\n}\n\n/** Create Agent **/\nconst httpAgent = new http.Agent({\n  keepAlive: true,\n});\nconst httpsAgent = new https.Agent({\n  keepAlive: true,\n});\n\nconst xiorInstance = xior.create({\n  baseURL: 'https://example.com',\n  fetch: nodeFetch,\n  // agent: httpAgent,\n  agent(_parsedURL) {\n    if (_parsedURL.protocol === 'http:') {\n      return httpAgent;\n    } else {\n      return httpsAgent;\n    }\n  },\n});\n```\n\n**Use `@tauri-apps/plugin-http`'s fetch implementaion in Tauri**:\n\n```ts\nimport { fetch } from '@tauri-apps/plugin-http';\nimport xior from 'xior';\n\nexport const http = xior.create({\n  baseURL: 'https://www.tauri.app',\n  fetch,\n});\n\nasync function test() {\n  const { data } = await http.get('/');\n  return data;\n}\n```\n\nFor `Taro`:\n\n```ts\nimport { fetch } from 'taro-fetch-polyfill';\nimport xior from 'xior';\n\n// fetch('https://api.github.com')\n//     .then(response =\u003e response.json())\n//     .then(console.log);\n\nexport const http = xior.create({\n  baseURL: 'https://github.com/NervJS/taro',\n  fetch,\n});\n\nasync function test() {\n  const { data } = await http.get('/');\n  return data;\n}\n```\n\n### 4. How do I handle responses with types like `'stream'`, `'document'`, `'arraybuffer'`, or `'blob'`?\n\nWhen `{responseType: 'blob'| 'arraybuffer'}`:\n\n```ts\nxior.get('https://exmaple.com/some/api', { responseType: 'blob' }).then((response) =\u003e {\n  console.log(response.data); // response.data is a Blob\n});\n\n// Same with\nfetch('https://exmaple.com/some/api')\n  .then((response) =\u003e response.blob())\n  .then((data) =\u003e {\n    console.log(data); // is a Blob\n  });\n```\n\n```ts\nxior.get('https://exmaple.com/some/api', { responseType: 'arraybuffer' }).then((response) =\u003e {\n  console.log(response.data); // response.data is a ArrayBuffer\n});\n\n// Same with\nfetch('https://exmaple.com/some/api')\n  .then((response) =\u003e response.arraybuffer())\n  .then((data) =\u003e {\n    console.log(data); // is a ArrayBuffer\n  });\n```\n\n**But when `responseType` set to `'stream', 'document', 'custom' or 'original'`, Xior will return the original fetch response and `res.data` will be undefined:**\n\n```ts\nfetch('https://exmaple.com/some/api').then((response) =\u003e {\n  console.log(response);\n});\n\n// same with\nxior.get('https://exmaple.com/some/api', { responseType: 'stream' }).then((res) =\u003e {\n  console.log(res.response); // But res.data will be undefined\n});\n```\n\nAnd to handle a stream response, use the `responseType: 'stream'` option in your request, then do something with the `response` as `fetch` does:\n\n```ts\nimport xior from 'xior';\n\nconst http = xior.create({ baseURL });\n\nconst { response } = await http.post\u003c{ file: any; body: Record\u003cstring, string\u003e }\u003e(\n  '/stream/10',\n  null,\n  { responseType: 'stream' }\n);\nconst reader = response.body!.getReader();\nlet chunk;\nfor await (chunk of readChunks(reader)) {\n  console.log(`received chunk of size ${chunk.length}`);\n}\n```\n\n### 5. How do I support older browsers?\n\nYou can use a polyfill for the `fetch` API. Check the file `src/tests/polyfill.test.ts` for a potential example.\n\n### 6. Why is xior named \"xior\"?\n\nThe original name `axior` was unavailable on npm, so when removed the \"a\": ~~a~~**xior**.\n\n### 7. Where can I ask additional questions?\n\nIf you have any questions, feel free to create issues.\n\n## Migrate from `axios` to **xior**\n\n**The most common change is replacing `axios` with `xior` and checking if the TypeScript types pass**:\n\n```ts\nimport axios, {\n  XiorError as AxiosError,\n  isXiorError as isAxiosError,\n  XiorRequestConfig as AxiosRequestConfig,\n  XiorResponse as AxiosResponse,\n} from 'xior';\n\nconst instance = axios.create({\n  baseURL: '...',\n  timeout: 20e3,\n});\n```\n\n### GET\n\naxios:\n\n```ts\nimport axios from 'axios';\n\n// Make a request for a user with a given ID\naxios.get('/user?ID=12345');\n\n// Optionally the request above could also be done as\naxios.get('/user', {\n  params: {\n    ID: 12345,\n  },\n});\n\n// Want to use async/await? Add the `async` keyword to your outer function/method.\nasync function getUser() {\n  try {\n    const response = await axios.get('/user?ID=12345');\n    console.log(response);\n  } catch (error) {\n    console.error(error);\n  }\n}\n```\n\nxior:\n\n```ts\nimport axios from 'xior';\n\n// Make a request for a user with a given ID\naxios.get('/user?ID=12345');\n\n// Optionally the request above could also be done as\naxios.get('/user', {\n  params: {\n    ID: 12345,\n  },\n});\n\n// Want to use async/await? Add the `async` keyword to your outer function/method.\nasync function getUser() {\n  try {\n    const response = await axios.get('/user?ID=12345');\n    console.log(response);\n  } catch (error) {\n    console.error(error);\n  }\n}\n```\n\n### POST\n\naxios:\n\n```ts\nimport axios from 'axios';\n\naxios.post('/user', {\n  firstName: 'Fred',\n  lastName: 'Flintstone',\n});\n```\n\nxior:\n\n```ts\nimport axios from 'xior';\n\naxios.post('/user', {\n  firstName: 'Fred',\n  lastName: 'Flintstone',\n});\n```\n\n### `axios(requestObj)`: axios({ method: 'get', params: { a: 1 } })\n\naxios:\n\n```ts\nimport axios from 'axios';\n\nawait axios({ method: 'get', params: { a: 1 } });\n```\n\nxior:\n\n```ts\nimport xior from 'xior';\n\nconst axios = xior.create();\n\nawait axios.request({ method: 'get', params: { a: 1 } });\n```\n\n### Creating an instance\n\naxios:\n\n```ts\nimport axios from 'axios';\nconst instance = axios.create({\n  baseURL: 'https://some-domain.com/api/',\n  timeout: 1000,\n  headers: { 'X-Custom-Header': 'foobar' },\n});\n```\n\nxior:\n\n```ts\nimport axios from 'xior';\nconst instance = axios.create({\n  baseURL: 'https://some-domain.com/api/',\n  timeout: 1000,\n  headers: { 'X-Custom-Header': 'foobar' },\n});\n```\n\n### Get response headers\n\naxios:\n\n```ts\nimport axios from 'axios';\n\nconst axiosInstance = axios.create({\n  baseURL: 'https://apiexampledomian.com/api',\n});\n\nconst { data, headers } = await axiosInstance.get('/');\n\nconsole.log(headers['X-Header-Name']);\n```\n\nxior:\n\n```ts\nimport xior from 'xior';\n\nconst xiorInstance = xior.create({\n  baseURL: 'https://apiexampledomian.com/api',\n});\n\nconst { data, headers } = await xiorInstance.get('/');\n\nconsole.log(headers.get('X-Header-Name'));\n```\n\n### Download file with `responseType: 'stream' | 'blob'`\n\naxios:\n\n```ts\nimport axios from 'axios';\nimport fs from 'fs';\n\n// GET request for remote image in Node.js\naxios({\n  method: 'get',\n  url: 'https://bit.ly/2mTM3nY',\n  responseType: 'stream',\n}).then(function (response) {\n  response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'));\n});\n\n// For browser\naxios({\n  method: 'get',\n  url: 'https://bit.ly/2mTM3nY',\n  responseType: 'blob',\n}).then(function (response) {\n  // create file link in browser's memory\n  const href = URL.createObjectURL(response.data);\n\n  // create \"a\" HTML element with href to file \u0026 click\n  const link = document.createElement('a');\n  link.href = href;\n  link.setAttribute('download', 'file.pdf'); //or any other extension\n  document.body.appendChild(link);\n  link.click();\n\n  // clean up \"a\" element \u0026 remove ObjectURL\n  document.body.removeChild(link);\n  URL.revokeObjectURL(href);\n});\n```\n\nxior:\n\n```ts\n// Node.js\nimport xior from 'xior';\nconst axios = xior.create();\n\naxios\n  .get('https://bit.ly/2mTM3nY', {\n    responseType: 'stream',\n  })\n  .then(async function ({ response, config }) {\n    const buffer = Buffer.from(await response.arrayBuffer());\n    return writeFile('ada_lovelace.jpg', buffer);\n  });\n\n// For browser\nxior\n  .get('https://d2l.ai/d2l-en.pdf', {\n    headers: {\n      Accept: 'application/pdf',\n    },\n    responseType: 'blob',\n  })\n  .then((res) =\u003e {\n    const { data: blob } = res;\n    var url = window.URL.createObjectURL(blob);\n    var a = document.createElement('a');\n    a.href = url;\n    a.download = 'filename.pdf';\n    document.body.appendChild(a); // we need to append the element to the dom -\u003e otherwise it will not work in firefox\n    a.click();\n    a.remove(); //afterwards we remove the element again\n  });\n```\n\n### Use stream\n\naxios:\n\n```ts\nimport axios from 'axios';\nimport { Readable } from 'stream';\n\nconst http = axios.create();\n\nasync function getStream(url: string, params: Record\u003cstring, any\u003e) {\n  const { data } = await http.get(url, {\n    params,\n    responseType: 'stream',\n  });\n  return data;\n}\n```\n\nxior:\n\n```ts\nimport axxios from 'xior';\nimport { Readable } from 'stream';\n\nconst http = axios.create();\n\nasync function getStream(url: string, params: Record\u003cstring, any\u003e) {\n  const { response } = await http.get(url, {\n    params,\n    responseType: 'stream',\n  });\n  const stream = convertResponseToReadable(response);\n  return stream;\n}\n\nfunction convertResponseToReadable(response: Response): Readable {\n  const reader = response.body.getReader();\n  return new Readable({\n    async read() {\n      const { done, value } = await reader.read();\n      if (done) {\n        this.push(null);\n      } else {\n        this.push(Buffer.from(value));\n      }\n    },\n  });\n}\n```\n\n## Migrate from `fetch` to **xior**\n\n### GET\n\nfetch:\n\n```ts\nasync function logMovies() {\n  const response = await fetch('http://example.com/movies.json?page=1\u0026perPage=10');\n  const movies = await response.json();\n  console.log(movies);\n}\n```\n\nxior:\n\n```ts\nimport xior from 'xior';\n\nconst http = xior.create({\n  baseURL: 'http://example.com',\n});\nasync function logMovies() {\n  const { data: movies } = await http.get('/movies.json', {\n    params: {\n      page: 1,\n      perPage: 10,\n    },\n  });\n  console.log(movies);\n}\n```\n\n### POST\n\nfetch:\n\n```ts\n// Example POST method implementation:\nasync function postData(url = '', data = {}) {\n  // Default options are marked with *\n  const response = await fetch(url, {\n    method: 'POST', // *GET, POST, PUT, DELETE, etc.\n    mode: 'cors', // no-cors, *cors, same-origin\n    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached\n    credentials: 'same-origin', // include, *same-origin, omit\n    headers: {\n      // 'Content-Type': 'application/json',\n      // 'Content-Type': 'application/x-www-form-urlencoded',\n    },\n    redirect: 'follow', // manual, *follow, error\n    referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url\n    body: JSON.stringify(data), // body data type must match \"Content-Type\" header\n  });\n  return response.json(); // parses JSON response into native JavaScript objects\n}\n\npostData('https://example.com/answer', { answer: 42 }).then((data) =\u003e {\n  console.log(data); // JSON data parsed by `data.json()` call\n});\n```\n\nxior:\n\n```ts\nimport xior from 'xior';\n\nconst http = xior.create({\n  baseURL: 'http://example.com',\n});\n\nhttp\n  .post(\n    '/answer',\n    { answer: 42 },\n    {\n      mode: 'cors', // no-cors, *cors, same-origin\n      cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached\n      credentials: 'same-origin', // include, *same-origin, omit\n      headers: {\n        // 'Content-Type': 'application/json',\n        // 'Content-Type': 'application/x-www-form-urlencoded',\n      },\n      redirect: 'follow', // manual, *follow, error\n      referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url\n    }\n  )\n  .then(({ data }) =\u003e {\n    console.log(data);\n  });\n```\n\n### Abort a fetch\n\nfetch:\n\n```ts\nconst controller = new AbortController();\nconst signal = controller.signal;\nconst url = 'video.mp4';\n\nconst downloadBtn = document.querySelector('#download');\nconst abortBtn = document.querySelector('#abort');\n\ndownloadBtn.addEventListener('click', async () =\u003e {\n  try {\n    const response = await fetch(url, { signal });\n    console.log('Download complete', response);\n  } catch (error) {\n    console.error(`Download error: ${error.message}`);\n  }\n});\n\nabortBtn.addEventListener('click', () =\u003e {\n  controller.abort();\n  console.log('Download aborted');\n});\n```\n\nxior:\n\n```ts\nimport xior from 'xior';\n\nconst http = xior.create();\n\nconst controller = new AbortController();\nconst signal = controller.signal;\nconst url = 'video.mp4';\n\nconst downloadBtn = document.querySelector('#download');\nconst abortBtn = document.querySelector('#abort');\n\ndownloadBtn.addEventListener('click', async () =\u003e {\n  try {\n    const response = await http.get(url, { signal });\n    console.log('Download complete', response);\n  } catch (error) {\n    console.error(`Download error: ${error.message}`);\n  }\n});\n\nabortBtn.addEventListener('click', () =\u003e {\n  controller.abort();\n  console.log('Download aborted');\n});\n```\n\n### Sending a request with credentials included\n\nfetch:\n\n```ts\nfetch('https://example.com', {\n  credentials: 'include',\n});\n```\n\nxior:\n\n```ts\nimport xior from 'xior';\n\nconst http = xior.create();\n\nhttp.get('https://example.com', {\n  credentials: 'include',\n});\n```\n\n### Uploading a file\n\nfetch:\n\n```ts\nasync function upload(formData) {\n  try {\n    const response = await fetch('https://example.com/profile/avatar', {\n      method: 'PUT',\n      body: formData,\n    });\n    const result = await response.json();\n    console.log('Success:', result);\n  } catch (error) {\n    console.error('Error:', error);\n  }\n}\n\nconst formData = new FormData();\nconst fileField = document.querySelector('input[type=\"file\"]');\n\nformData.append('username', 'abc123');\nformData.append('avatar', fileField.files[0]);\n\nupload(formData);\n```\n\nxior:\n\n```ts\nimport xior from 'xior';\n\nconst http = xior.create({\n  baseURL: 'https://example.com',\n});\n\nasync function upload(formData) {\n  try {\n    const { data: result } = await http.put('/profile/avatar', formData);\n    console.log('Success:', result);\n  } catch (error) {\n    console.error('Error:', error);\n  }\n}\n\nconst formData = new FormData();\nconst fileField = document.querySelector('input[type=\"file\"]');\n\nformData.append('username', 'abc123');\nformData.append('avatar', fileField.files[0]);\n\nupload(formData);\n```\n\n### Processing a text file line by line\n\nfetch:\n\n```ts\nasync function* makeTextFileLineIterator(fileURL) {\n  const utf8Decoder = new TextDecoder('utf-8');\n  const response = await fetch(fileURL);\n  const reader = response.body.getReader();\n  let { value: chunk, done: readerDone } = await reader.read();\n  chunk = chunk ? utf8Decoder.decode(chunk) : '';\n\n  const newline = /\\r?\\n/gm;\n  let startIndex = 0;\n  let result;\n\n  while (true) {\n    const result = newline.exec(chunk);\n    if (!result) {\n      if (readerDone) break;\n      const remainder = chunk.substr(startIndex);\n      ({ value: chunk, done: readerDone } = await reader.read());\n      chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : '');\n      startIndex = newline.lastIndex = 0;\n      continue;\n    }\n    yield chunk.substring(startIndex, result.index);\n    startIndex = newline.lastIndex;\n  }\n\n  if (startIndex \u003c chunk.length) {\n    // Last line didn't end in a newline char\n    yield chunk.substr(startIndex);\n  }\n}\n\nasync function run() {\n  for await (const line of makeTextFileLineIterator(urlOfFile)) {\n    processLine(line);\n  }\n}\n\nrun();\n```\n\nxior:\n\n\u003e Good to Know: add `{responseType: 'stream'}` options will tell xior no need process response, and return original response in format `{response}`\n\n```ts\nimport xior from 'xior';\n\nconst http = xior.create();\n\nasync function* makeTextFileLineIterator(fileURL) {\n  const utf8Decoder = new TextDecoder('utf-8');\n  const { response } = await http.get(fileURL, { responseType: 'stream' });\n  const reader = response.body.getReader();\n  let { value: chunk, done: readerDone } = await reader.read();\n  chunk = chunk ? utf8Decoder.decode(chunk) : '';\n\n  const newline = /\\r?\\n/gm;\n  let startIndex = 0;\n  let result;\n\n  while (true) {\n    const result = newline.exec(chunk);\n    if (!result) {\n      if (readerDone) break;\n      const remainder = chunk.substr(startIndex);\n      ({ value: chunk, done: readerDone } = await reader.read());\n      chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : '');\n      startIndex = newline.lastIndex = 0;\n      continue;\n    }\n    yield chunk.substring(startIndex, result.index);\n    startIndex = newline.lastIndex;\n  }\n\n  if (startIndex \u003c chunk.length) {\n    // Last line didn't end in a newline char\n    yield chunk.substr(startIndex);\n  }\n}\n\nasync function run() {\n  for await (const line of makeTextFileLineIterator(urlOfFile)) {\n    processLine(line);\n  }\n}\n\nrun();\n```\n\n## API Reference\n\n- https://suhaotian.github.io/xior\n\n- https://www.jsdocs.io/package/xior\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=suhaotian/xior\u0026type=Date)](https://star-history.com/#suhaotian/xior\u0026Date)\n\n## Thanks\n\nWithout the support of these resources, xior wouldn't be possible:\n\n- [axios](https://github.com/axios/axios)\n- [axios-extensions](https://github.com/kuitos/axios-extensions)\n- [axios-mock-adapter](https://github.com/ctimmerm/axios-mock-adapter)\n- ~~[ts-deepmerge](https://github.com/voodoocreation/ts-deepmerge)~~\n- [tiny-lru](https://github.com/avoidwork/tiny-lru)\n- ~~[bunchee](https://github.com/huozhi/bunchee)~~ [tsup](https://tsup.egoist.dev)\n- [fetch MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsuhaotian%2Fxior","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsuhaotian%2Fxior","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsuhaotian%2Fxior/lists"}