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

<Meta title='@dxtmisha/vi/functional-basic/# Api'/>

# Api

Lớp `Api` là công cụ trung tâm để thực hiện các yêu cầu HTTP. Nó cung cấp một giao diện đơn giản cho các phương thức phổ biến và quản lý các cài đặt toàn cục như URL cơ sở, tiêu đề mặc định và các hook vòng đời.

### Các khả năng chính

Lớp cung cấp một bộ công cụ toàn diện để quản lý tương tác mạng:
- **Các phương thức CRUD** — thực hiện các yêu cầu `GET`, `POST`, `PUT`, `PATCH`, `DELETE`.
- **Cấu hình toàn cục** — thiết lập URL cơ sở, nguồn (origin), tiêu đề và tham số.
- **Hook vòng đời** — xử lý các sự kiện trước và sau yêu cầu (ví dụ: cho Refresh Token).
- **Hydration (SSR)** — chuyển dữ liệu từ máy chủ sang máy khách để ngăn chặn các yêu cầu trùng lặp.
- **Bộ nhớ đệm (Caching)** — lưu các phản hồi trong bộ nhớ hoặc bộ nhớ ngoài (LocalStorage).
- **Giả lập phản hồi** — tạo dữ liệu giả để phát triển và kiểm thử.
- **Tự động bản địa hóa** — thay thế các nhãn `{locale}`, `{country}` và `{language}` trong đường dẫn yêu cầu.
- **Quản lý tải và lỗi** — tích hợp với các thành phần chỉ báo toàn cục và trung tâm lỗi.

## Cách dùng cơ bản

Cách đơn giản nhất để thực hiện yêu cầu là sử dụng các phương thức tĩnh `get`, `post`, `put`, hoặc `delete`. Tất cả các phương thức đều hỗ trợ định nghĩa kiểu dữ liệu (typing) thông qua Generic.

```javascript
import { Api } from '@dxtmisha/functional-basic'

// Yêu cầu GET đơn giản với định nghĩa kiểu
const data = await Api.get<User[]>({ path: 'users' })

// Yêu cầu POST kèm dữ liệu trong body
const newUser = await Api.post<User>({
  path: 'users',
  request: { name: 'Nguyễn Văn A', job: 'Lập trình viên' }
})
```

## Cấu hình toàn cục

Bạn có thể cấu hình hành vi của API trên toàn hệ thống, ví dụ như khi khởi tạo ứng dụng. Việc này giúp loại bỏ sự cần thiết phải chỉ định các đường dẫn đầy đủ và các tiêu đề chung trong mỗi lần gọi.

```javascript
// Đặt URL cơ sở cho tất cả các đường dẫn tương đối
Api.setUrl('/api/')

// Thiết lập nguồn (giao thức và tên miền) cho URL cơ sở
// Nếu urlRoot bắt đầu bằng '/', origin sẽ được thêm vào trước nó
Api.setOrigin('https://api.example.com')

// Thêm các tiêu đề chung (ví dụ: token xác thực)
Api.setHeaders({
  'Authorization': 'Bearer YOUR_TOKEN'
})

// Đặt các tham số chung sẽ được thêm vào mọi yêu cầu
Api.setRequestDefault({
  app_version: '1.0.0'
})

// Kiểm tra xem máy chủ có đang chạy trên localhost không
if (Api.isLocalhost()) {
  console.log('Đang chạy trên máy cục bộ')
}
```

### Phương thức setConfig

Cho phép thiết lập tất cả các cài đặt chính bằng một đối tượng duy nhất.

```javascript
Api.setConfig({
  urlRoot: '/api/',
  origin: 'https://api.example.com',
  headers: { 'Authorization': 'Bearer ...' },
  timeout: 5000,
  devMode: true
})
```

## Xử lý URL

Khi tạo URL cuối cùng, lớp sẽ tự động thay thế các nhãn đặc biệt bằng các giá trị hiện tại từ cài đặt bản địa hóa (`Geo`):
- `{locale}` — locale hiện tại (ví dụ: `vi-VN`).
- `{country}` — quốc gia hiện tại (ví dụ: `vn`).
- `{language}` — ngôn ngữ hiện tại (ví dụ: `vi`).

```javascript
// Nếu locale hiện tại là 'vi-VN', đường dẫn cuối cùng sẽ là '/api/vi-VN/users'
const data = await Api.get({ path: '{locale}/users' })
```

## Trạng thái và Thông tin meta

Lớp `Api` tự động trích xuất các siêu dữ liệu (metadata) từ phản hồi của máy chủ (ví dụ: `success`, `status`, `message`, `code`) và hợp nhất chúng với đối tượng dữ liệu của bạn. Điều này cho phép bạn nhận được cả nội dung dữ liệu và kết quả thực thi trong một đối tượng duy nhất.

Bạn có thể truy cập trạng thái đầy đủ của yêu cầu cuối cùng thông qua phương thức `getStatus()`, phương thức này trả về một instance của `ApiStatus`.

```javascript
const data = await Api.get({ path: 'users' })
const status = Api.getStatus()

console.log(status.getStatus())     // 200 (Trạng thái HTTP)
console.log(status.getStatusType()) // 'success' | 'error' | 'warning' | 'info'
console.log(status.getMessage())    // Thông báo từ máy chủ hoặc văn bản trạng thái
console.log(status.getResponse())   // Nội dung phản hồi thô đã được phân tích
```

## Nhận dạng lỗi

Thư viện cung cấp một hệ thống quản lý lỗi tinh vi thông qua `ApiError`. Bạn có thể đăng ký trước các mẫu lỗi API đã biết, và hệ thống sẽ tự động xác định chúng dựa trên trạng thái, mã lỗi hoặc các hàm xác thực tùy chỉnh.

```javascript
import { ApiError, ApiMethodItem } from '@dxtmisha/functional-basic'

// Đăng ký một mẫu lỗi cụ thể
ApiError.add({
  url: 'users/login',
  method: ApiMethodItem.post,
  code: 'INVALID_CREDENTIALS',
  message: 'Vui lòng kiểm tra lại tên đăng nhập và mật khẩu.'
})

// Xử lý yêu cầu
const status = Api.getStatus()
const errorItem = status.get()?.errorObject // Instance ApiErrorItem

if (errorItem && errorItem.getCode() === 'INVALID_CREDENTIALS') {
  // Xử lý trường hợp lỗi cụ thể
}
```

## Hook vòng đời

Thư viện cho phép bạn chèn logic toàn cục sẽ được kích hoạt cho mọi yêu cầu. Việc này rất hữu ích để thêm token động, ghi log hoặc xử lý tập trung các lỗi máy chủ.

### Hook chuẩn bị (setPreparation)

Được thực thi ngay trước khi yêu cầu được gửi đến máy chủ.

- **Tham số**: Nhận đối tượng `apiFetch`, chứa tất cả các tham số của yêu cầu hiện tại (đường dẫn, phương thức, dữ liệu, v.v.).
- **Sử dụng**: Kiểm tra quyền truy cập, thêm các tham số động vào `apiFetch.request`, hoặc đơn giản là ghi log bắt đầu một hoạt động.

```javascript
Api.setPreparation(async (apiFetch) => {
  // Bạn có thể sửa đổi apiFetch trước khi gửi
  console.log(`Bắt đầu yêu cầu đến: ${apiFetch.path}`)
})
```

### Hook kết thúc (setEnd)

Được thực thi sau khi máy chủ trả về phản hồi, nhưng trước khi dữ liệu đến mã của bạn.

- **Tham số**: Nhận đối tượng `Response` gốc và đối tượng cài đặt `apiFetch`.
- **Trả về**: Một Promise với đối tượng `{ reset?: boolean, data?: any }`.
- **Cờ reset**: Nếu trả về `true`, thư viện sẽ hủy chuỗi hiện tại và thực hiện lại yêu cầu. Việc này rất hữu ích để tự động xử lý token hết hạn (Refresh Token).
- **Trường data**: Cho phép bạn thay thế hoặc bổ sung dữ liệu được trả về bởi phương thức `Api.request`.

```javascript
Api.setEnd(async (response, apiFetch) => {
  // Ví dụ: xử lý lỗi 401 Unauthorized
  if (response.status === 401) {
    const isRefreshed = await myAuthService.refreshToken()
    
    if (isRefreshed) {
      return { reset: true } // Thử lại yêu cầu với token mới
    }
  }
  
  return {} // Tiếp tục xử lý tiêu chuẩn
})
```

## Hydration

Hydration cho phép chuyển dữ liệu thu được trong quá trình server-side rendering (SSR) sang phía máy khách. Điều này ngăn chặn việc thực hiện lại các yêu cầu tương tự ngay sau khi trang tải xong.

1. **Trên máy chủ**: Thực hiện các yêu cầu cần thiết và lấy script qua `Api.getHydrationScript()`.
2. **Trong mẫu (Template)**: Chèn chuỗi kết quả vào HTML.
3. **Trên máy khách**: Khi khởi tạo `Api`, dữ liệu sẽ tự động được tải vào trình quản lý phản hồi.

```javascript
// Ví dụ SSR
await Api.get({ path: 'settings' });
const hydrationScript = Api.getHydrationScript(); // <script>...</script>
```

## Bộ nhớ đệm (Caching)

Lớp `Api` hỗ trợ tự động lưu bộ nhớ đệm cho các phản hồi. Bạn có thể sử dụng bộ nhớ trong tích hợp hoặc kết nối bộ nhớ ngoài (ví dụ: `LocalStorage`).

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

// Kết nối bộ nhớ ngoài (tùy chọn)
ApiCache.init(
  async (key) => DataStorage.get(key),
  async (key, value) => DataStorage.set(key, value)
);

// Sử dụng trong một yêu cầu
const data = await Api.get({
  path: 'catalog/list',
  cache: 3600 // Lưu bộ nhớ đệm trong 1 giờ (tính bằng giây)
});
```

## Giả lập phản hồi

Trình quản lý `ApiResponse` tích hợp sẵn (có thể truy cập qua `Api.getResponse()`) cho phép bạn chặn các yêu cầu và trả về dữ liệu giả (mock). Điều này cực kỳ hữu ích cho việc phát triển giao diện khi backend chưa sẵn sàng hoặc để viết các bài kiểm thử.

### Đăng ký Mock

Bạn có thể thêm cả phản hồi tĩnh và các hàm động.

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

// 1. Phản hồi tĩnh cho yêu cầu GET
responseManager.add({
  path: 'profile',
  method: 'GET',
  response: { id: 1, name: 'Admin' },
  lag: true // Mô phỏng độ trễ mạng (0-2 giây)
})

// 2. Phản hồi động dựa trên các tham số yêu cầu
responseManager.add({
  path: 'users/search',
  method: 'GET',
  response: (request) => {
    return {
      results: [`Kết quả cho: ${request.query}`],
      count: 1
    }
  }
})

// 3. Chặn yêu cầu POST với bất kỳ body nào
responseManager.add({
  path: 'users/update',
  method: 'POST',
  request: '*any', // Bỏ qua việc kiểm tra body yêu cầu
  response: { success: true }
})
```

### Chế độ phát triển

Để gỡ lỗi dễ dàng hơn, bạn có thể bật tính năng ghi nhận (logging) giả lập trong console. Trong chế độ này, trình quản lý sẽ cảnh báo trong console về mỗi yêu cầu bị chặn.

```javascript
Api.getResponse().setDevMode(true)
```

Trình giả lập sẽ kích hoạt **trước khi** lệnh `fetch` thực sự được thực hiện. Nếu không tìm thấy mock phù hợp, `Api` sẽ thực hiện một yêu cầu mạng thông thường.

## Nâng cao: ApiInstance

Lớp `Api` là một **static proxy** cho một instance toàn cục của `ApiInstance`. Instance này được lưu trữ trong `ServerStorage`, đảm bảo sự cô lập giữa các yêu cầu trong quá trình SSR và hành vi singleton trên máy khách.

Đối với các kịch bản nâng cao—chẳng hạn như kết nối với nhiều API độc lập khác nhau cùng một lúc hoặc tạo các môi trường cô lập tạm thời—bạn có thể khởi tạo `ApiInstance` trực tiếp.

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

const secondaryApi = new ApiInstance()
secondaryApi.setUrl('https://another-api.com/')
secondaryApi.setHeaders({ 'X-Client-Id': '...' })

const data = await secondaryApi.get({ path: 'resource' })
```
