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

<Meta title='@dxtmisha/en/functional-basic/Classes/ApiInstance - Request Management'/>

# ApiInstance Class

A class operating over the Fetch API for efficient network request operations. Provides advanced configuration capabilities, global status handling, response emulation, preparation hooks setup, and automatic data parsing.

It includes tight integration with dependency classes: `ApiStatus`, `ApiResponse`, `ApiPreparation`, `ApiDefault`, and `ApiHeaders`.

## Key Features

- **CRUD Methods** — convenient aliases `get`, `post`, `put`, `patch`, `delete` for handling HTTP requests
- **Global Configuration** — setting base URL, default headers, and parameters for all requests
- **Lifecycle Hooks** — ability to set custom callback functions executed before (`setPreparation`) and after (`setEnd`) each request
- **Status Tracking** — access to the status and errors of the last request via `ApiStatus`
- **Hydration** — transferring data from server to client (SSR) for instant rendering
- **Caching** — saving responses in internal or external storage via `ApiCache`
- **Response Emulation** — flexible management of mock data via `ApiResponse`
- **Automatic Parsing** — converting the request body into JSON/FormData and automatically extracting data from JSON responses
- **Geolocation** — automatic substitution of `{locale}`, `{country}`, `{language}` placeholders into request paths

## Constructor

The `ApiInstance` can be configured during initialization with custom dependency classes or specific instances.

**Parameters:**
- `url: string` — base path for all requests (default: `'/api/'`).
- `options: ApiInstanceOptions` — configuration object for dependency injection.

### `ApiInstanceOptions`

- `headersClass?: typeof ApiHeaders` — class for managing headers.
- `requestDefaultClass?: typeof ApiDefault` — class for global request parameters.
- `statusClass?: typeof ApiStatus` — class for tracking request status.
- `responseClass?: typeof ApiResponse` — class for response emulation and caching.
- `preparationClass?: typeof ApiPreparation` — class for lifecycle hook management.
- `loadingClass?: LoadingInstance` — instance of the loading indicator manager (defaults to `Loading.getItem()`).
- `errorCenterClass?: ErrorCenterInstance` — instance of the error center for triggering events (defaults to `ErrorCenter.getItem()`).
- `hydrationClass?: typeof ApiHydration` — class for SSR hydration management.

## Methods

### Action

#### `request`

Core executor sequence method managing pure physical network fetch pipelines. Should a solitary string serve as param context, default `GET` assumptions trigger automatically.
In case of network failures (e.g., 500 status), the method explicitly throws an exception (`throw e`), allowing errors to be natively intercepted in `try/catch`. If the server returns a JSON object, the system automatically injects a `statusObject` field (instance of request status) to the returned payload.

**Parameters:**
- `pathRequest: string | ApiFetch` — destination path matching format or root configuration structure object.

**Returns:** `Promise<ApiData<T>>` — decoded endpoint structural response formatted interface (including `data`, `statusObject`, etc., when returning an object payload).

### Shorthand

- `get(request: ApiFetch): Promise<T>` — Dispatches a basic query through HTTP `GET` methodologies natively optimizing inputs payload onto string search parameters formats automatically.
- `post(request: ApiFetch): Promise<T>` — Transfers query packets across standard REST/GraphQL pipelines formatted onto the HTTP `POST` specification.
- `put(request: ApiFetch): Promise<T>` — Directs object modifications over target remote points using typical system environment HTTP `PUT`.
- `patch(request: ApiFetch): Promise<T>` — Initiates partial resource updates using the standard HTTP `PATCH` method.
- `delete(request: ApiFetch): Promise<T>` — Forwards structural clearance/purge request using classic URL targeted HTTP `DELETE`.

### Check

- `isLocalhost(): boolean` — Returns `true` if the code is running on a local development server.
- `getStatus(): ApiStatus` — Returns the `ApiStatus` manager for tracking the state, errors, and complete response object of the last network request.
- `getResponse(): ApiResponse` — Returns the responses/emulation manager `ApiResponse` for controlling mock-responses.

### Configuration

Methods to configure the API (support chaining, returning the `ApiInstance` object):
- `setHeaders(headers: Record<string, string>): this` — Applies universal headers injected automatically toward outbound fetch activities.
- `setRequestDefault(request: Record<string, any>): this` — Forces parameters recursively sent to network traffic streams.
- `setUrl(url: string): this` — Overwrites generic target URL configurations prefix points.
- `setPreparation(callback: (apiFetch: ApiFetch) => Promise<void>): this` — Connects a custom hook function actively running just _before_ execution trigger occurs.
- `setEnd(callback: (query: Response, apiFetch: ApiFetch) => Promise<ApiPreparationEnd>): this` — Sets the hook triggered exactly _after_ network response returns.
- `setOrigin(origin: string): this` — Sets the origin (protocol and domain) for the base URL.

### Auxiliary

- `getUrl(path: string, api?: boolean): string` — Generates the full target URL referencing the script, handling `{locale}`, `{country}`, `{language}` placeholder substitution.
- `getOrigin(): string` — Returns the base origin URL combined with the API path.
- `getBody(request?: ApiFetch['request'], method?: string): string | FormData | undefined` — Creates data to be passed into the request physical body.
- `getBodyForGet(request: ApiFetch['request'], path?: string, method?: string): string` — Forms query parameters specifically for HTTP arguments assigned into the URL directly used when sending `GET` queries.
- `getHydration(): ApiHydration` — Returns the hydration manager.
- `getHydrationScript(): string` — Returns a string representation of the hydration data for the client.

## ApiFetch Object

`ApiFetch` is the main configuration object passed to all request methods (`request`, `get`, `post`, `put`, `patch`, `delete`). All fields are optional:

- `path?: string` — endpoint path relative to the base URL (e.g. `'users/list'`).
- `pathFull?: string` — full request URL. If provided, overrides `api` and `path`.
- `api?: boolean` — whether to prepend the base URL before `path`. Default: `true`.
- `method?: ApiMethod` — HTTP method (`'GET'`, `'POST'`, `'PUT'`, `'PATCH'`, `'DELETE'`). Default: `'GET'`.
- `request?: FormData | Record<string, any> | string` — request body (for POST/PUT/PATCH) or query parameters (for GET). Serialized as JSON or `FormData`.
- `headers?: Record<string, string> | null` — additional request headers. Pass `null` to disable all headers.
- `type?: string` — `Content-Type` header value. Default: `'application/json;charset=UTF-8'`.
- `auth?: boolean` — include authentication headers.
- `toData?: boolean` — if `true`, automatically unwraps `data` from a `{ data: ... }` wrapper. Default: `true`.
- `global?: boolean` — use global response cache.
- `devMode?: boolean` — enable development logging.
- `hideError?: boolean` — suppresses `console.error` output on failure. Default: `false`.
- `globalPreparation?: boolean` — whether to run the global `setPreparation` hook before this request. Default: `true`.
- `globalEnd?: boolean` — whether to run the global `setEnd` hook after the response. Default: `true`.
- `queryReturn?: (query: Response) => Promise<any>` — custom response processor instead of the default JSON reader.
- `init?: RequestInit` — additional options passed directly to native `fetch()` (CORS, cache mode, etc.).
- `timeout?: number` — request timeout in milliseconds. Default: `16000`ms (only used if `controller` is not provided).
- `controller?: AbortController` — controller for canceling the request. If provided, timeout is ignored.
- `retry?: number` — number of retries on failure.
- `retryDelay?: number` — base delay between retries in milliseconds. Default: `64`ms. Jitter is used to prevent thundering herd problems.
- `endResetLimit?: number` — limit for recursive retries via `setEnd` with `reset: true`. Default: `8`.

## Response Data Structure

Requests return a `Promise<ApiData<T>>`.

### `ApiData<T>`

The structure of the returned data depends on the type of response:
- **Array**: If the response is an array, `ApiData<T>` acts as `T[]`.
- **Object**: If the response is an object, `ApiData<T>` includes fields from `T` and the standard wrapper `ApiDataItem<T>`.

### `ApiDataItem<T>`

The object response contains the following fields:

- `data?: T` — primary payload if the response was wrapped.
- `success?: boolean` — success flag.
- `status?: ApiStatusType` — status type (`'success'`, `'error'`, `'warning'`, `'info'`).
- `message?: string` — response message or error description.
- `statusObject?: ApiStatusItem` — technical information about the request:
    - `status?: number` — HTTP status code (e.g., 200, 404).
    - `statusText?: string` — HTTP status text.
    - `error?: string` — internal error message if the request failed.
    - `lastResponse?: any` — data from the last response before parsing.
    - `lastStatus?: ApiStatusType` — status of the last request.
    - `lastMessage?: string` — message from the last request.

## Request Cancellation (AbortController)

The API supports out-of-the-box request cancellation. Pass an instance of `AbortController` into the `controller` field of the `ApiFetch` configuration.

```javascript
const controller = new AbortController()

// Dispatch the request
api.get({ path: 'users', controller }).catch(e => {
  if (e.name === 'AbortError') {
    console.log('Request was cancelled by the user')
  }
})

// Cancel the request later (e.g., when the component unmounts)
controller.abort()
```

## Lifecycle Hooks

Hooks allow you to inject global logic that will automatically execute for every network request. This is a powerful tool for tasks such as:
- Dynamically adding authorization tokens.
- Centralized logging of network activity.
- Automatically refreshing expired tokens (Refresh Token).
- Global user notification of errors.

### Request Preparation (`setPreparation`)

The `setPreparation` method sets a callback function that is called immediately before a request is sent to the server (before the native `fetch` is executed).

**Key Features:**
- **Execution Order**: If multiple requests are started simultaneously, the class ensures that the preparation hook is executed **sequentially**. All subsequent requests will wait for the current hook to finish in wait mode (with a 160ms polling interval). This prevents conflicts, such as when multiple concurrent requests attempt to refresh a token.
- **Parameters**: Receives the `apiFetch` object, which contains all current request settings. You can modify this object (e.g., adding headers or modifying the request body).
- **Returns**: `Promise<void>`.

```javascript
api.setPreparation(async (apiFetch) => {
  // Example: adding a dynamic header
  if (apiFetch.auth) {
    const token = await authStore.getToken()
    apiFetch.headers = {
      ...apiFetch.headers,
      'Authorization': `Bearer ${token}`
    }
  }
  
  console.log(`[API] Preparing request: ${apiFetch.method} ${apiFetch.path}`)
})
```

### Request Completion (`setEnd`)

The `setEnd` method sets a callback function that is called after receiving a response from the server, but before the data is passed to the calling code.

**Parameters:**
- `query: Response` — the native Fetch API response object.
- `apiFetch: ApiFetch` — the parameters object with which the request was performed.

**Returns:** `Promise<ApiPreparationEnd>`. The result object can contain the following fields:
- `reset?: boolean` — if set to `true`, the library will abort the current chain and **execute the request again** (recursively) after a jittered delay (see `retryDelay`). The entire process of preparation and execution will be restarted. This is used for automatic handling of refreshed access rights. This recursion is limited by `endResetLimit` (default: 8).
- `data?: any` — allows you to override the data returned by the API method. If this field is specified, the standard response reading mechanism (JSON/text) will be ignored.

```javascript
api.setEnd(async (response, apiFetch) => {
  // Example: automatic Refresh Token update
  if (response.status === 401) {
    const isSuccess = await authStore.refresh()
    
    if (isSuccess) {
      // Retry the request with the new token
      return { reset: true }
    }
  }

  // Example: global error notification
  if (response.status >= 500) {
    notification.error('Service is temporarily unavailable')
  }

  return {}
})
```

### Disabling Hooks for a Specific Request

You can selectively disable global hooks in the `ApiFetch` object:
- `globalPreparation: false` — skip the `setPreparation` hook execution.
- `globalEnd: false` — skip the `setEnd` hook execution.

## Hydration

Hydration is a mechanism for transferring data obtained during Server-Side Rendering (SSR) to the client side. This avoids re-executing the same network requests immediately after the page loads in the browser, which significantly speeds up rendering and reduces server load.

### How it works

1. **Server-side (SSR):**
   - All executed requests (usually `GET` methods with `global: true`) are automatically saved in the hydration list.
   - After the page is formed, you need to call `api.getHydrationScript()` and insert the resulting string into the HTML template (usually at the end of the `<body>` tag).

2. **Client-side (Browser):**
   - When an `ApiInstance` is created, it automatically looks for the hydration script on the page.
   - Found data is loaded into the system and substituted for the first call of similar requests.
   - Thus, the client code receives data instantly without waiting for a network response.

### Usage Example (SSR)

```javascript
// On the server
const api = new ApiInstance();
await api.get({ path: 'settings' });

// In the page template (e.g., at the end of body)
// The method returns a string: <script id="__ui:api:hydration:id__" type="application/json">[...]</script>
const hydrationScript = api.getHydrationScript();
```


## Caching

The library provides a built-in mechanism for caching responses, allowing you to save request results in memory or in external storage (e.g., `LocalStorage`).

### Key Features

- **Time to Live (TTL)** — for each request, you can specify the cache validity time in seconds.
- **External Storage** — ability to integrate with any asynchronous data storage.
- **Automatic Cleanup** — old data is removed from memory when limits are exceeded.

### Setting up External Storage

By default, the cache works only in RAM and is reset when the page is reloaded. To save data long-term, you need to initialize `ApiCache` with your own listeners (for example, using `LocalStorage` or `IndexedDB`):

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

// Example of initialization using LocalStorage via DataStorage wrapper
ApiCache.init(
  async (key) => DataStorage.get(key),
  async (key, value) => {
    DataStorage.set(key, value);
    return true;
  },
  async (key) => {
    DataStorage.remove(key);
    return true;
  }
);
```

### Usage in Requests

To enable caching, just pass the time in seconds in the `cache` field of the `ApiFetch` object:

```javascript
// Cache response for 1 hour (3600 seconds)
const data = await api.get({
  path: 'catalog/list',
  cache: 3600
});
```

If the data is in the cache and its lifetime has not expired, the library will return it instantly without performing a real request to the server.

## Response Emulation

The emulator manager `ApiResponse` (accessible via `api.getResponse()`) allows configuring mock responses for specified paths. This is incredibly useful during frontend UI development without a completely ready backend API or for writing test fixtures.

### Registering Mock Responses

The `add` method accepts an `ApiResponseItem` object or an array of such objects.

**Key `ApiResponseItem` fields:**
- `path` — String (exact match) or Regular Expression (`RegExp`) to intercept the request URL.
- `method` — HTTP method (`ApiMethodItem.get`, `post`, `put`, `patch`, etc.).
- `request` — (Optional) Request body filter. You can use the special marker `'*any'` to bypass payload validation and capture all requests to this path.
- `response` — Static data object _or_ a dynamic factory function `(request) => any` that generates a response based on the incoming request payload.
- `disable` — (Optional) Disable this mock (boolean or callback).
- `isForGlobal` — (Optional) Mark as global mock.
- `lag` — (Optional) Simulates network lag (`boolean`), adding a random `setTimeout` from 0 to 2000ms.

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

// Enable developer mode (prints mock request logs and intercepted requests into the console)
responseManager.setDevMode(true)

// Example 1: Static response for a GET request
responseManager.add({
  path: 'users/profile', // can be a string or RegExp
  method: 'GET',
  response: { id: 1, name: 'Admin', role: 'superuser' },
  lag: true // Add artificial lag to simulate real network ping
})

// Example 2: Dynamic response with a factory curve (useful for pagination or search simulation)
responseManager.add({
  path: /users\/search/, // intercepting using RegExp
  method: 'GET',
  // The function receives the original payload object passed via request
  response: (request) => {
    return [
        { id: 1, name: `Results for: ${request.query}` }
    ]
  }
})

// Example 3: Intercept POST request regardless of body payload
responseManager.add({
  path: 'users/create',
  method: 'POST',
  request: '*any', // the *any marker bypasses request payload checking altogether
  response: { success: true, message: 'User created successfully (MOCK)' }
})

// Usage in code (no real network stream is opened, mock resolves instantly or after simulated lag)
const profile = await api.get({ path: 'users/profile' })
```

### Mock Prioritization and Fallback

- The emulator intercepts requests **before** the actual `fetch` call is made.
- If the emulator finds no matching registered mock (due to `path`, `method`, or `request` mismatch), the class automatically falls back to executing a real physical network request to the remote server.
- You can conditionally disable a specific mock by providing a callback to the `disable: () => true` field. The `add` engine will ignore it.

## Examples

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

const api = new ApiInstance()

// API Configuration
api.setUrl('https://api.example.com/v1/')
   .setHeaders({ 'Authorization': 'Bearer token123' })
   .setRequestDefault({ source: 'webapp' })

// Preparation interceptor hook
api.setPreparation(async (apiFetch) => {
    console.log('Initializing request to:', apiFetch.path)
})

// Standard GET fetch behavior with error handling
try {
  const data = await api.get({ path: 'users' })
  // Extracting inline status payload gracefully:
  // console.log(data.statusObject.getStatus())
} catch (e) {
  console.error('Network transport error while pulling users:', e)
}

// Extended POST behavior capable of abort termination
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('Request safely aborted!')
}

// Abort when needed
// abortCtrl.abort()
```
