import {Meta} from '@storybook/addon-docs/blocks'

<Meta title='@dxtmisha/ru/functional-basic/Classes/ApiInstance - Управление запросами'/>

# Класс ApiInstance

Класс для работы с сетевыми запросами через Fetch API. Предоставляет расширенные возможности конфигурирования, глобальной обработки статусов, эмуляции ответов, установки хуков подготовки и автоматического парсинга данных. 

Включает тесную интеграцию с зависимыми классами: `ApiStatus`, `ApiResponse`, `ApiPreparation`, `ApiDefault` и `ApiHeaders`.

## Ключевые особенности

- **CRUD методы** — удобные алиасы `get`, `post`, `put`, `patch`, `delete` для работы с HTTP-запросами
- **Глобальная конфигурация** — установка базового URL, заголовков и параметров по умолчанию для всех запросов
- **Хуки жизненного цикла** — возможность задать функции, выполняемые до (`setPreparation`) и после (`setEnd`) каждого запроса
- **Отслеживание статуса** — доступ к статусу и ошибкам последнего запроса через `ApiStatus`
- **Гидратация** — передача данных от сервера к клиенту (SSR) для мгновенной отрисовки
- **Кэширование** — сохранение ответов во внутреннем или внешнем хранилище через `ApiCache`
- **Эмуляция ответов** — гибкое управление мок-данными через `ApiResponse`
- **Автоматический парсинг** — преобразование тела запроса в JSON/FormData и автоматическое извлечение данных из JSON-ответов
- **Геолокация** — автоматическая подстановка `{locale}`, `{country}`, `{language}` в пути запросов

## Конструктор

Экземпляр `ApiInstance` может быть настроен во время инициализации с помощью пользовательских классов зависимостей или конкретных экземпляров.

**Параметры:**
- `url: string` — базовый путь для всех запросов (по умолчанию: `'/api/'`).
- `options: ApiInstanceOptions` — объект конфигурации для внедрения зависимостей.

### `ApiInstanceOptions`

- `headersClass?: typeof ApiHeaders` — класс для управления заголовками.
- `requestDefaultClass?: typeof ApiDefault` — класс для параметров запроса по умолчанию.
- `statusClass?: typeof ApiStatus` — класс для отслеживания статуса запросов.
- `responseClass?: typeof ApiResponse` — класс для эмуляции ответов и кэширования.
- `preparationClass?: typeof ApiPreparation` — класс для управления хуками жизненного цикла.
- `loadingClass?: LoadingInstance` — экземпляр менеджера индикации загрузки (по умолчанию: `Loading.getItem()`).
- `errorCenterClass?: ErrorCenterInstance` — экземпляр центра ошибок для вызова событий (по умолчанию: `ErrorCenter.getItem()`).
- `hydrationClass?: typeof ApiHydration` — класс для управления гидратацией SSR.

## Методы

### Действие

#### `request`

Универсальный метод для выполнения сетевого HTTP-запроса. Если передана строка, выполняется простой `GET` запрос по этому адресу.
В случае сетевых сбоев (например, 500 статус) метод явно выбрасывает исключение (`throw e`), что позволяет перехватывать ошибки в `try/catch`. Если сервер возвращает JSON объект, к возвращаемым данным автоматически добавляется поле `statusObject` (инстанс статуса запроса).

**Параметры:**
- `pathRequest: string | ApiFetch` — путь (строка) или конфигурационный объект запроса.

**Возвращает:** `Promise<ApiData<T>>` — возвращает спарсенные данные ответа сервера (с полями `data`, `statusObject` и др., если возвращается объект).

### Сокращенные методы

- `get(request: ApiFetch): Promise<T>` — Отправляет запрос методом `GET`.
- `post(request: ApiFetch): Promise<T>` — Отправляет запрос методом `POST`. Идеально для создания/сохранения сущностей.
- `put(request: ApiFetch): Promise<T>` — Отправляет запрос методом `PUT`. Применяется для обновления данных.
- `patch(request: ApiFetch): Promise<T>` — Отправляет запрос методом `PATCH`. Используется для частичного обновления данных ресурса.
- `delete(request: ApiFetch): Promise<T>` — Отправляет запрос методом `DELETE`. Применяется для удаления данных из системы.

### Проверка

- `isLocalhost(): boolean` — Возвращает `true`, если код запущен на локальном сервере.
- `getStatus(): ApiStatus` — Возвращает менеджер `ApiStatus` для работы со статусом последнего запроса.
- `getResponse(): ApiResponse` — Возвращает менеджер эмуляции `ApiResponse`.

### Конфигурация

Методы для настройки API (поддерживают чейнинг вызовов, возвращают объект `ApiInstance`):
- `setHeaders(headers: Record<string, string>): this` — Устанавливает глобальные заголовки по умолчанию.
- `setRequestDefault(request: Record<string, any>): this` — Устанавливает общие параметры для всех запросов.
- `setUrl(url: string): this` — Изменяет базовый URL путь для API скриптов.
- `setPreparation(callback: (apiFetch: ApiFetch) => Promise<void>): this` — Устанавливает хук, выполняемый перед каждым сетевым запросом.
- `setEnd(callback: (query: Response, apiFetch: ApiFetch) => Promise<ApiPreparationEnd>): this` — Устанавливает хук, выполняемый после получения ответа.
- `setOrigin(origin: string): this` — Устанавливает источник (протокол и домен) для формирования базового URL.

### Вспомогательные

- `getUrl(path: string, api?: boolean): string` — Формирует полный адрес к скрипту запроса с подстановкой плейсхолдеров `{locale}`, `{country}`, `{language}`.
- `getOrigin(): string` — Возвращает базовый URL источника, объединенный с путем API.
- `getBody(request?: ApiFetch['request'], method?: string): string | FormData | undefined` — Формирует данные для тела запроса.
- `getBodyForGet(request: ApiFetch['request'], path?: string, method?: string): string` — Формирует строку query-параметров для GET-запроса.
- `getHydration(): ApiHydration` — Возвращает менеджер гидратации.
- `getHydrationScript(): string` — Возвращает строковое представление данных гидратации для клиента.

## Объект ApiFetch

`ApiFetch` — главный конфигурационный объект, передаваемый в методы `request`, `get`, `post`, `put`, `patch`, `delete`. Все поля опциональны:

- `path?: string` — путь к endpoint-скрипту относительно базового URL (например: `'users/list'`).
- `pathFull?: string` — полный URL запроса. Если указан, игнорирует `api` и `path`.
- `api?: boolean` — добавлять ли базовый URL перед `path`. По умолчанию: `true`.
- `method?: ApiMethod` — HTTP-метод (`'GET'`, `'POST'`, `'PUT'`, `'PATCH'`, `'DELETE'`). По умолчанию: `'GET'`.
- `request?: FormData | Record<string, any> | string` — тело запроса (для POST/PUT/PATCH) или query-параметры (for GET). Передается как JSON или `FormData`.
- `headers?: Record<string, string> | null` — дополнительные заголовки запроса. `null` — отключить все заголовки.
- `type?: string` — значение `Content-Type`. По умолчанию: `'application/json;charset=UTF-8'`.
- `auth?: boolean` — добавить заголовки аутентификации.
- `toData?: boolean` — если `true`, автоматически разворачивает `data` из обертки `{ data: ... }`. По умолчанию: `true`.
- `global?: boolean` — использовать глобальный кэш ответов.
- `devMode?: boolean` — включить логирование разработки.
- `hideError?: boolean` — подавляет вывод ошибок в `console.error` при сбое. По умолчанию: `false`.
- `globalPreparation?: boolean` — запускать ли глобальный хук `setPreparation` до этого запроса. По умолчанию: `true`.
- `globalEnd?: boolean` — запускать ли глобальный хук `setEnd` после ответа. По умолчанию: `true`.
- `queryReturn?: (query: Response) => Promise<any>` — обработчик ответа вместо стандартного чтения JSON.
- `init?: RequestInit` — дополнительные опции для нативного `fetch()` (CORS, режим кеша и др.).
- `timeout?: number` — таймаут запроса в миллисекундах. По умолчанию: `16000`мс (используется только если не передан `controller`).
- `controller?: AbortController` — контроллер для отмены запроса. Если передан, таймаут игнорируется.
- `retry?: number` — количество повторов при сбое.
- `retryDelay?: number` — базовая задержка между повторами в миллисекундах. По умолчанию: `64`мс. Используется алгоритм с джиттером для предотвращения лавинообразных запросов.
- `endResetLimit?: number` — лимит рекурсивных повторов через `setEnd` с `reset: true`. По умолчанию: `8`.

## Структура данных ответа

Запросы возвращают `Promise<ApiData<T>>`.

### `ApiData<T>`

Структура возвращаемых данных зависит от типа ответа:
- **Массив**: Если ответ является массивом, `ApiData<T>` представляет собой `T[]`.
- **Объект**: Если ответ является объектом, `ApiData<T>` включает в себя поля из `T` и стандартную обертку `ApiDataItem<T>`.

### `ApiDataItem<T>`

Объект ответа содержит следующие поля:

- `data?: T` — основная полезная нагрузка, если ответ был обернут.
- `success?: boolean` — флаг успешности.
- `status?: ApiStatusType` — тип статуса (`'success'`, `'error'`, `'warning'`, `'info'`).
- `message?: string` — сообщение ответа или описание ошибки.
- `statusObject?: ApiStatusItem` — техническая информация о запросе:
    - `status?: number` — HTTP-код статуса (например, 200, 404).
    - `statusText?: string` — текст статуса HTTP.
    - `error?: string` — внутреннее сообщение об ошибке, если запрос не удался.
    - `lastResponse?: any` — данные последнего ответа до парсинга.
    - `lastStatus?: ApiStatusType` — статус последнего запроса.
    - `lastMessage?: string` — сообщение последнего запроса.

## Отмена запросов (AbortController)

API поддерживает отмену запросов из коробки. Для этого можно передать экземпляр `AbortController` в поле `controller` объекта `ApiFetch`.

```javascript
const controller = new AbortController()

// Запускаем запрос, передавая контроллер
api.get({ path: 'users', controller }).catch(e => {
  if (e.name === 'AbortError') {
    console.log('Запрос был отменен пользователем')
  }
})

// Отменяем при размонтировании компонента (например)
controller.abort()
```

## Хуки жизненного цикла (Lifecycle Hooks)

Хуки позволяют внедрять логику, которая будет автоматически выполняться для каждого сетевого запроса данного инстанса. Это мощный инструмент для решения таких задач, как:
- Динамическое добавление токенов авторизации.
- Централизованное логирование сетевой активности.
- Автоматическое обновление истекших токенов (Refresh Token).
- Глобальное уведомление пользователя об ошибках.

### Подготовка запроса (`setPreparation`)

Метод `setPreparation` устанавливает callback-функцию, которая вызывается непосредственно перед отправкой запроса на сервер (до выполнения нативного `fetch`).

**Особенности работы:**
- **Очередность исполнения**: Если одновременно запускается несколько запросов, класс гарантирует, что хук подготовки будет выполнен **последовательно**. Все последующие запросы будут ждать завершения текущего хука в режиме ожидания (с интервалом опроса 16мс). Это предотвращает конфликты, например, при одновременном обновлении токена несколькими запросами.
- **Параметры**: Получает объект `apiFetch`, содержащий все текущие настройки запроса. Вы можете изменять этот объект (например, добавляя заголовки или модифицируя тело запроса).
- **Возвращает**: `Promise<void>`.

```javascript
api.setPreparation(async (apiFetch) => {
  // Пример: добавление динамического заголовка
  if (apiFetch.auth) {
    const token = await authStore.getToken()
    apiFetch.headers = {
      ...apiFetch.headers,
      'Authorization': `Bearer ${token}`
    }
  }
  
  console.log(`[API] Подготовка запроса: ${apiFetch.method} ${apiFetch.path}`)
})
```

### Завершение запроса (`setEnd`)

Метод `setEnd` устанавливает callback-функцию, которая вызывается после получения ответа от сервера, но перед тем, как данные будут переданы в вызывающий код.

**Параметры:**
- `query: Response` — нативный объект ответа Fetch API.
- `apiFetch: ApiFetch` — объект параметров, с которыми был выполнен запрос.

**Возвращает:** `Promise<ApiPreparationEnd>`. Объект результата может содержать следующие поля:
- `reset?: boolean` — если установлено в `true`, библиотека прервет текущую цепочку и **выполнит запрос заново** (рекурсивно) через рандомизированную задержку (см. `retryDelay`). Весь процесс подготовки и выполнения будет запущен повторно. Используется для автоматической обработки обновленных прав доступа. Лимит таких повторов ограничен параметром `endResetLimit` (по умолчанию 8).
- `data?: any` — позволяет переопределить данные, которые вернет метод API. Если это поле указано, стандартный механизм чтения ответа (JSON/текст) будет проигнорирован.

```javascript
api.setEnd(async (response, apiFetch) => {
  // Пример: автоматическое обновление Refresh Token
  if (response.status === 401) {
    const isSuccess = await authStore.refresh()
    
    if (isSuccess) {
      // Повторить запрос с новым токеном
      return { reset: true }
    }
  }

  // Пример: глобальное уведомление об ошибке
  if (response.status >= 500) {
    notification.error('Сервис временно недоступен')
  }

  return {}
})
```

### Отключение хуков для конкретного запроса

Вы можете выборочно отключить выполнение глобальных хуков в объекте `ApiFetch`:
- `globalPreparation: false` — пропустить выполнение хука `setPreparation`.
- `globalEnd: false` — пропустить выполнение хука `setEnd`.

## Гидратация

Гидратация — это механизм передачи данных, полученных в ходе серверного рендеринга (SSR), на сторону клиента. Это позволяет избежать повторного выполнения тех же самых сетевых запросов сразу после загрузки страницы в браузере, что значительно ускоряет отрисовку и снижает нагрузку на сервер.

### Как это работает

1. **На стороне сервера (SSR):**
   - Все выполненные запросы (обычно метода `GET` с параметром `global: true`) автоматически сохраняются в списке гидратации.
   - После завершения формирования страницы необходимо вызвать метод `api.getHydrationScript()` и вставить полученную строку в HTML-шаблон (обычно в конец тега `<body>`).

2. **На стороне клиента (Браузер):**
   - При создании экземпляра `ApiInstance` он автоматически ищет на странице скрипт с данными гидратации.
   - Найденные данные загружаются в систему и подставляются вместо первого вызова аналогичных запросов.
   - Таким образом, клиентский код получает данные мгновенно, не дожидаясь ответа от сети.

### Пример использования (SSR)

```javascript
// На сервере
const api = new ApiInstance();
await api.get({ path: 'settings' });

// В шаблоне страницы (например, в конце body)
// Метод вернет строку: <script id="__ui:api:hydration:id__" type="application/json">[...]</script>
const hydrationScript = api.getHydrationScript();
```


## Кэширование

Библиотека предоставляет встроенный механизм кэширования ответов, который позволяет сохранять результаты запросов в памяти или во внешнем хранилище (например, `LocalStorage`).

### Основные возможности

- **Время жизни (TTL)** — для каждого запроса можно указать время актуальности кэша в секундах.
- **Внешние хранилища** — возможность интеграции с любым асинхронным хранилищем данных.
- **Автоматическая очистка** — старые данные удаляются из памяти при превышении лимитов.

### Настройка внешнего хранилища

По умолчанию кэш работает только в оперативной памяти и сбрасывается при перезагрузке страницы. Чтобы сохранять данные надолго, необходимо инициализировать `ApiCache` своими слушателями (например, используя `LocalStorage` или `IndexedDB`):

```javascript
import { ApiCache, DataStorage } from '@dxtmisha/functional';

// Пример инициализации с использованием LocalStorage через обертку DataStorage
ApiCache.init(
  async (key) => DataStorage.get(key),
  async (key, value) => {
    DataStorage.set(key, value);
    return true;
  },
  async (key) => {
    DataStorage.remove(key);
    return true;
  }
);
```

### Использование в запросах

Для включения кэширования достаточно передать время в секундах в поле `cache` объекта `ApiFetch`:

```javascript
// Кэшировать ответ на 1 час (3600 секунд)
const data = await api.get({
  path: 'catalog/list',
  cache: 3600
});
```

Если данные есть в кэше и их время жизни не истекло, библиотека вернет их мгновенно, не выполняя реальный запрос к серверу.

## Эмуляция ответов

Менеджер эмуляции `ApiResponse` (доступен через `api.getResponse()`) позволяет настраивать мок-ответы для определенных путей — это невероятно полезно при разработке UI без готового Backend API или для написания тестов.

### Регистрация мок-ответов

Метод `add` принимает объект типа `ApiResponseItem` или массив таких объектов.

**Важные поля `ApiResponseItem`:**
- `path` — Строка (точное совпадение) или Регулярное выражение (`RegExp`) для перехвата пути.
- `method` — HTTP метод (`ApiMethodItem.get`, `post`, `put`, `patch` и т.д.).
- `request` — (Опционально) Фильтр по телу запроса. Можно использовать специальный маркер `'*any'`, чтобы перехватывать любые данные.
- `response` — Статический объект с данными ответа _или_ функция `(request) => any`, генерирующая ответ динамически на основе параметров запроса.
- `disable` — (Опционально) Отключить этот мок (boolean или callback).
- `isForGlobal` — (Опционально) Пометить как глобальный мок.
- `lag` — (Опционально) Имитировать задержку сети (`boolean`), добавляет случайный setTimeout от 0 до 2000мс.

```javascript
const responseManager = api.getResponse()

// Включить режим разработчика (в консоль будут выводиться логи перехваченных запросов)
responseManager.setDevMode(true)

// Пример 1: Статический ответ на GET запрос
responseManager.add({
  path: 'users/profile', // можно использовать строку или RegExp
  method: 'GET',
  response: { id: 1, name: 'Admin', role: 'superuser' },
  lag: true // Искусственная задержка (lag) для симуляции реального сетевого пинга
})

// Пример 2: Динамический ответ с функцией (полезно для пагинации или поиска)
responseManager.add({
  path: /users\/search/, // перехватываем по RegExp
  method: 'GET',
  // Функция получает оригинальные параметры, переданные в request
  response: (request) => {
    return [
        { id: 1, name: `Искали: ${request.query}` }
    ]
  }
})

// Пример 3: Перехват POST запроса с любым телом
responseManager.add({
  path: 'users/create',
  method: 'POST',
  request: '*any', // маркер *any позволяет игнорировать проверку тела запроса
  response: { success: true, message: 'Пользователь успешно создан (MOCK)' }
})

// Использование в коде (сетевой запрос не уйдет, вернется мок)
const profile = await api.get({ path: 'users/profile' })
```

### Приоритет и фильтрация моков

- Эмулятор (mock) срабатывает **до** реального `fetch`.
- Если эмулятор не находит совпадений (по `path`, `method` или `request`), класс выполнит реальный сетевой запрос к серверу.
- Вы можете отключить определенный мок, передав функцию в поле `disable: () => true`. Метод `add` не будет его перехватывать.

## Примеры

```javascript
import { ApiInstance } from '@dxtmisha/functional'

const api = new ApiInstance()

// Конфигурация API
api.setUrl('https://api.example.com/v1/')
   .setHeaders({ 'Authorization': 'Bearer token123' })
   .setRequestDefault({ source: 'webapp' })

// Отслеживание подготовки перед началом (hook)
api.setPreparation(async (apiFetch) => {
    console.log('Начинается запрос к:', apiFetch.path)
})

// Простой GET запрос с обработкой ошибок
try {
  const data = await api.get({ path: 'users' })
  // Из объекта ответа также можно извлечь встроенный статус (если возвращается объект):
  // console.log(data.statusObject.getStatus())
} catch (e) {
  console.error('Сетевая ошибка при запросе пользователей:', e)
}

// POST запрос с телом и возможностью отмены
const abortCtrl = new AbortController()
try {
  const newUser = await api.post({
    path: 'users/create',
    request: { name: 'John Doe', age: 30 },
    controller: abortCtrl
  })
} catch (e) {
  if (e.name === 'AbortError') console.log('Запрос отменен!')
}

// Отмена в случае необходимости
// abortCtrl.abort()
```
