///
///
import { IStringifyOptions } from 'qs';
import { Readable } from 'stream';
import { RequestOptions } from 'http';
/**
* Akita HTTP Client
*
* 多平台 HTTP 客户端,支持 Node.js、浏览器和微信小程序
*
* ## 架构特点(供 AI Agent 参考)
*
* ### 1. Promise 风格 API
* Request 类继承自 Promise,可直接 await 或链式调用 then/catch/finally
* - 支持 Promise.all、Promise.race 等标准 Promise 操作
* - 请求对象既可当 Promise 使用,也可访问元数据属性
*
* ### 2. 自动 JSON 解析
* - 默认使用 JSON.parse() 解析响应体
* - 自动检测服务器返回的 `error` 字段(值为 '0'/'null'/'none' 时忽略)
* - 支持 Reducer 函数在解析后转换数据
*
* ### 3. Promise 缓存机制
* 为避免重复解析,Request 对象内部缓存多个 Promise:
* - `_responsePromise`: 原始 Response Promise
* - `_textPromise`: 文本解析 Promise
* - `_bufferPromise`: Buffer 解析 Promise
* - `_blobPromise`: Blob 解析 Promise
* - `_dataPromise`: JSON 解析 Promise
* - `_jsPromise`: JsonStream 解析 Promise
*
* ### 4. 钩子执行顺序
* 钩子按以下顺序执行:
* 1. onEncode → Body 编码前(可修改 body)
* 2. onRequest → 请求发送前(可修改 init)
* 3. fetch() → 实际发送请求
* 4. onResponse → 接收到响应后(可访问 res)
* 5. onDecode → 响应体解析后(可修改 value)
*
* ### 5. 进度跟踪模型
* - 每个请求:3 步(编码、发送、接收)
* - 进度范围:0-1
* - 完成后的请求保留 1 秒历史
* - 支持防抖更新(5ms 间隔)
*
* ### 6. 平台适配策略
* 通过依赖注入(inject)解耦平台差异:
* - Node.js: 使用 node-fetch 和 form-data
* - 浏览器: 使用 window.fetch 和 window.FormData
* - 微信小程序: 使用 wx.request 和自定义 fetch polyfill
*
* ### 7. 错误处理层次
* - Network Error: DNS 失败、超时、连接拒绝等(type='network')
* - HTTP Error: 4xx/5xx 状态码(type='http')
* - Parse Error: JSON/text 解析失败(type='parse')
* - Server Error: 服务器返回 {error:'msg'}(type='server')
*
* ## 特点:
* - Promise 风格 API
* - 自动 JSON 解析
* - 多种响应格式支持(JSON、文本、Buffer、流)
* - 自动查询参数序列化
* - 自动表单数据转换
* - 文件上传支持
* - 钩子系统
* - 多客户端实例
* - 客户端配置共享
* - 自动 URL 拼装
*
* @example
* ```typescript
* import akita from 'akita';
*
* // 简单的 GET 请求
* const users = await akita.get('/api/users');
*
* // 带查询参数的 GET 请求
* const users = await akita.get('/api/users', { query: { page: 1, limit: 10 } });
*
* // POST 请求
* const user = await akita.post('/api/users', { body: { name: 'John', age: 30 } });
* ```
*
* @packageDocumentation
*/
/**
* 数据处理器,对返回的数据进行预处理,用于自定义增减数据字段
*
* ## 执行时机
* Reducer 在以下时机执行:
* 1. 响应体被 JSON.parse() 解析后
* 2. 在 onDecode 钩子之前执行
* 3. 仅当使用 `request(path, init, reducer)` 方法时生效
*
* ## AI Agent 使用建议
* - Reducer 可以用于统一数据格式转换(如 snake_case → camelCase)
* - 可用于数据验证,抛出错误阻止请求成功
* - 常用于提取嵌套的 data 字段
* - 在 jsonStream 中仅作用于 `json.object`
*
* ## Reducer 函数在服务器响应解析为 JSON 后执行,可用于:
* - 转换数据格式(如日期字符串转 Date 对象)
* - 过滤或重命名字段
* - 提取嵌套数据
* - 数据验证
*
* @example
* ```typescript
* // 提取嵌套的 data 字段
* const response = await client.get('/api/users', {}, (json) => json.data);
*
* // 转换日期格式
* const user = await client.get('/api/user/1', {}, (json) => ({
* ...json,
* createdAt: new Date(json.createdAt)
* }));
*
* // 提取并重命名字段
* const data = await client.get('/api/data', {}, (json) => ({
* id: json.user_id,
* name: json.user_name
* }));
* ```
*/
export interface Reducer {
(json: any): T;
}
/**
* JSON 数据流(NDJSON - Newline Delimited JSON)
*
* 用于处理服务器返回的流式 JSON 数据,每行一个 JSON 对象
*
* @example
* ```typescript
* // 使用 read() 逐行读取
* const stream = await client.get('/api/events').jsonStream();
* let event = await stream.read();
* while (event) {
* console.log(event);
* event = await stream.read();
* }
*
* // 使用事件监听
* const stream = await client.get('/api/events').jsonStream();
* stream.on('data', (event) => {
* console.log('Received event:', event);
* });
* stream.on('error', (err) => {
* console.error('Stream error:', err);
* });
* stream.on('close', () => {
* console.log('Stream closed');
* });
* ```
*
* @see {@link ClientOptions.onDecode} 可结合 Reducer 使用
*/
export interface LineStream {
readonly closed: boolean;
read(): Promise;
on(event: 'data', fn: (data: T) => void): this;
on(event: 'error', fn: (error: Error) => void): this;
on(event: 'close', fn: () => void): this;
removeListener(name: string, fn: Function): this;
removeAllListeners(name: string): this;
close(): void;
}
export interface JsonStream extends LineStream {}
export interface SSEEvent {
type: string;
data: string;
id?: string;
retry?: number;
}
export interface SSEStream extends LineStream {
lastEventId: string;
retryInterval: number;
}
/**
* 请求参数(RequestInit)
*
* 这是每个请求可以配置的参数,与浏览器 Fetch API 的 RequestInit 基本兼容
*
* @example
* ```typescript
* // 基本用法
* const response = await client.get('/api/users', {
* query: { page: 1, limit: 10 },
* headers: { 'Authorization': 'Bearer token' }
* });
*
* // 完整配置
* const response = await client.request('/api/data', {
* method: 'POST',
* query: { debug: true },
* body: { name: 'test' },
* headers: { 'Content-Type': 'application/json' },
* signal: abortController.signal,
* timeout: 5000
* });
* ```
*/
export interface RequestInit {
/**
* 请求路径
*
* 相对于 client.options.apiRoot 的路径
* 如果 apiRoot 不存在,则为完整 URL
*
* @example
* ```typescript
* // 假设 apiRoot = 'https://api.example.com'
* client.get('/users') // https://api.example.com/users
* client.get('users') // https://api.example.com/users
* client.get('users/') // https://api.example.com/users/
* ```
*/
path?: string;
/**
* HTTP query 参数对象
*
* 自动使用 qs 库进行序列化,支持嵌套对象和数组
* 可通过 client.options.qsOptions 配置序列化行为
*
* @example
* ```typescript
* // 简单查询参数
* client.get('/users', { query: { page: 1, limit: 10 } })
* // -> /users?page=1&limit=10
*
* // 嵌套对象
* client.get('/users', { query: { filter: { name: 'John', age: 30 } } })
* // -> /users?filter[name]=John&filter[age]=30
*
* // 数组
* client.get('/users', { query: { ids: [1, 2, 3] } })
* // -> /users?ids[0]=1&ids[1]=2&ids[2]=3
*
* // URL 中已有参数
* client.get('/users?active=true', { query: { page: 1 } })
* // -> /users?active=true&page=1
* ```
*/
query?: any;
/**
* 请求方法,默认 GET
*
* HTTP 方法:GET, POST, PUT, PATCH, DELETE 等
*
* @example
* ```typescript
* client.get('/users') // GET
* client.post('/users', { method: 'POST' }) // POST
* client.request('/users', { method: 'PUT' }) // PUT
* ```
*/
method?: string;
/**
* 请求 Body 数据
*
* 根据数据类型自动转换为合适格式:
* - File/Blob/Uint8Array/ReadableStream -> 保持原样(用于文件上传)
* - 普通对象 + Content-Type=application/json -> JSON 字符串
* - 普通对象 + Content-Type=application/x-www-form-urlencoded -> URL 编码字符串
* - 包含 File 的对象 -> FormData
*
* @example
* ```typescript
* // JSON 请求(默认)
* client.post('/users', { body: { name: 'John', age: 30 } })
* // Content-Type: application/json
* // Body: {"name":"John","age":30}
*
* // 表单提交
* client.post('/login', {
* body: { username: 'admin', password: '123456' },
* headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
* })
* // Content-Type: application/x-www-form-urlencoded
* // Body: username=admin&password=123456
*
* // 文件上传(自动转换为 FormData)
* client.post('/upload', { body: { file: fileInput.files[0] } })
*
* // Buffer 上传
* client.post('/upload', { body: Buffer.from('hello') })
*
* // 字符串上传
* client.post('/echo', { body: 'hello world' })
* ```
*/
body?: any;
/**
* HTTP 请求 Headers
*
* 普通对象,键值对形式
* 与 client.options.init.headers 合并,请求 headers 优先级更高
*
* @example
* ```typescript
* client.get('/api/data', {
* headers: {
* 'Authorization': 'Bearer token123',
* 'Accept': 'application/json',
* 'X-Custom-Header': 'custom-value'
* }
* })
* ```
*/
headers?: any;
/**
* 请求模式(浏览器 Fetch API)
*
* - cors: 允许跨域请求(默认)
* - navigate: 导航请求
* - no-cors: 不允许跨域,响应类型不透明
* - same-origin: 同源请求
*/
mode?: 'cors' | 'navigate' | 'no-cors' | 'same-origin';
/**
* 凭证模式(浏览器 Fetch API)
*
* - omit: 不发送 cookie(默认)
* - include: 包含跨域 cookie
* - same-origin: 仅同源请求包含 cookie
*/
credentials?: 'omit' | 'include' | 'same-origin';
/**
* 重定向处理模式
*
* Node.js 专用
* - follow: 跟随重定向(默认)
* - manual: 手动处理重定向,可从响应头获取重定向信息
* - error: 遇到重定向时 reject
*
* @default 'follow'
* @example
* ```typescript
* // 手动处理重定向
* const res = await client.get('/redirect', { redirect: 'manual' })
* .response();
* const location = res.headers.get('Location');
* ```
*/
redirect?: 'follow' | 'manual' | 'error';
/**
* 请求中止信号
*
* Node.js 专用,用于取消正在进行的请求
*
* @example
* ```typescript
* const controller = new AbortController();
* const timeoutId = setTimeout(() => controller.abort(), 5000);
*
* try {
* const data = await client.get('/api/slow', {
* signal: controller.signal
* });
* } catch (error) {
* if (error.name === 'AbortError') {
* console.log('Request was aborted');
* }
* } finally {
* clearTimeout(timeoutId);
* }
* ```
*/
signal?: AbortSignal;
/**
* 最大重定向次数
*
* Node.js 专用
* 0 表示不跟随重定向
*
* @default 20
*/
follow?: number;
/**
* 请求超时时间(毫秒)
*
* Node.js 专用
* 遇到重定向时会重置超时
* 0 表示禁用超时(使用系统限制)
* 推荐使用 signal 代替
*
* @default 0
* @example
* ```typescript
* // 5 秒超时
* await client.get('/api/data', { timeout: 5000 });
* ```
*/
timeout?: number;
/**
* 是否启用压缩
*
* Node.js 专用
* 支持 gzip/deflate 内容编码
* false 表示禁用压缩
*
* @default true
*/
compress?: boolean;
/**
* 最大响应体大小(字节)
*
* Node.js 专用
* 0 表示不限制
*
* @default 0
*/
size?: number;
/**
* HTTP Agent
*
* Node.js 专用
* 用于连接池管理、Keep-Alive 等
*
* @example
* ```typescript
* import http from 'http';
*
* const agent = new http.Agent({ keepAlive: true });
* await client.get('/api/data', { agent });
*
* // 动态返回 agent
* await client.get('/api/data', {
* agent: (url) => {
* if (url.protocol === 'https:') {
* return new https.Agent({ keepAlive: true });
* }
* return new http.Agent({ keepAlive: true });
* }
* });
* ```
*/
agent?: RequestOptions['agent'] | ((parsedUrl: URL) => RequestOptions['agent']);
}
/**
* Request 请求类
*
* Request 类继承自 Promise,既可以像 Promise 一样使用,
* 也可以访问丰富的元数据和方法获取不同格式的响应
*
* @example
* ```typescript
* // 作为 Promise 使用(自动 JSON 解析)
* const users = await client.get('/api/users');
*
* // 访问元数据
* const request = client.get('/api/users');
* console.log(request.url); // 完整 URL
* console.log(request.init); // 请求配置
*
* // 等待完成后访问响应
* await request;
* console.log(request.res); // Response 对象
* console.log(request.raw); // 原始响应文本
* console.log(request.value); // 解析后的数据
*
* // 获取不同格式的响应
* const text = await client.get('/api/text').text();
* const buffer = await client.get('/api/image').buffer();
* const blob = await client.get('/api/file').blob();
* const stream = await client.get('/api/stream').stream();
* const jsonStream = await client.get('/api/events').jsonStream();
* ```
*/
export interface Request extends Promise {
/**
* 内部使用:请求步骤计数(用于进度计算)
* @internal
*/
_steps: number;
/**
* 内部使用:请求结束时间戳
* @internal
*/
_endAt?: number;
/**
* 请求所属的 Client 实例
*/
client: Client;
/**
* 请求的完整 URL 地址
*
* 包含 apiRoot 和查询参数的完整 URL
*
* @example
* ```typescript
* const request = client.get('/users', { query: { page: 1 } });
* console.log(request.url);
* // 假设 apiRoot = 'https://api.example.com'
* // 输出: https://api.example.com/users?page=1
* ```
*/
url: string;
/**
* 请求的 fetch init 参数
*
* 包含合并后的请求配置(client.options.init + 请求级配置)
*
* @example
* ```typescript
* const request = client.get('/users', { query: { page: 1 } });
* console.log(request.init);
* // { method: 'GET', query: { page: 1 }, ... }
* ```
*/
init: RequestInit;
/**
* 返回的 Response 对象
*
* 在请求完成后可用,包含标准 Fetch API Response 对象
*
* @example
* ```typescript
* const request = client.get('/users');
* await request;
* console.log(request.res?.status); // 200
* console.log(request.res?.headers); // Headers 对象
* console.log(request.res?.ok); // true
* ```
*/
res?: Response;
/**
* 返回的原始字符串结果
*
* 在调用 text() 或 data() 方法后可用
*
* @example
* ```typescript
* const request = client.get('/users');
* await request.text();
* console.log(request.raw);
* // [{"id":1,"name":"John"},...]
* ```
*/
raw?: string;
/**
* 返回的解析过的 JS 对象结果
*
* 在调用 data() 方法后可用,经过 onDecode 钩子处理
*
* @example
* ```typescript
* const request = client.get('/users');
* await request;
* console.log(request.value);
* // [{id: 1, name: "John"}, ...]
* ```
*/
value?: R;
/**
* 获取请求的返回数据,自动调用 JSON 解码
*
* .data() 调用可省略,直接 await request 即可
* 返回值会经过 onDecode 钩子和 Reducer 处理
*
* @returns Promise 解析后的 JSON 数据
*
* @example
* ```typescript
* // 方式 1:直接 await(推荐)
* const users = await client.get('/api/users');
*
* // 方式 2:显式调用 data()
* const users = await client.get('/api/users').data();
*
* // 使用 Reducer
* const users = await client.get('/api/users', {}, (json) => json.data);
* ```
*/
data(): Promise;
/**
* 获取返回的原始字符串
*
* 不进行 JSON 解析,直接返回响应文本
*
* @returns Promise 响应文本
*
* @example
* ```typescript
* const html = await client.get('/page').text();
* console.log(html); // ...
* ```
*/
text(): Promise;
/**
* 获取返回的原始 Buffer 数据
*
* 适用于下载二进制文件(图片、PDF 等)
*
* @returns Promise Buffer 对象
*
* @example
* ```typescript
* const buffer = await client.get('/image.png').buffer();
* fs.writeFileSync('image.png', buffer);
* ```
*/
buffer(): Promise;
/**
* 获取返回的原始 Blob 数据
*
* 浏览器端可用,类似于 buffer()
*
* @returns Promise Blob 对象
*
* @example
* ```typescript
* const blob = await client.get('/image.png').blob();
* const imageUrl = URL.createObjectURL(blob);
* document.getElementById('img').src = imageUrl;
* ```
*/
blob(): Promise;
/**
* 获取返回的数据流
*
* Node.js 返回 Readable,浏览器返回 ReadableStream
* 微信小程序不支持(会抛出错误)
*
* @returns Promise 流对象
*
* @example
* ```typescript
* const stream = await client.get('/large-file.zip').stream();
* stream.pipe(fs.createWriteStream('file.zip'));
* ```
*/
stream(): Promise;
/**
* 获取返回的 JSON 数据流
*
* 服务端应返回每行一个 JSON 对象(NDJSON 格式)
* 返回 JsonStream 对象,支持逐行读取或事件监听
*
* @template T JSON 数据类型
* @returns Promise> JsonStream 对象
*
* @example
* ```typescript
* // 使用 read() 逐行读取
* const stream = await client.get('/api/events').jsonStream();
let event = await stream.read();
while (event) {
* console.log(event);
* event = await stream.read();
* }
*
* // 使用事件监听
* const stream = await client.get('/api/events').jsonStream();
* stream.on('data', (event) => {
* console.log('Received:', event);
* });
* stream.on('close', () => console.log('Done'));
*
* // 使用 Reducer 转换数据
* const stream = await client.get('/api/events').jsonStream();
* // Reducer 可以在 onDecode 中配置
* ```
*/
jsonStream(): Promise>;
/**
* 获取文本数据流,按 \n 分割
*
* 每次读取返回一行文本
*
* @returns Promise> 文本行流
*
* @example
* ```typescript
* const stream = await client.get('/api/logs').lineStream();
* let line = await stream.read();
* while (line !== undefined) {
* console.log(line);
* line = await stream.read();
* }
* ```
*/
lineStream(): Promise>;
/**
* 获取 SSE (Server-Sent Events) 数据流
*
* 解析 SSE 格式的事件流,支持字段:data、event、id、retry
*
* @returns Promise SSEStream 对象
*
* @example
* ```typescript
* const stream = await client.get('/api/chat').sseStream();
* let event = await stream.read();
* while (event) {
* console.log(`[${event.type}]`, event.data);
* event = await stream.read();
* }
* ```
*/
sseStream(): Promise;
/**
* 获取返回的原始 Response 对象
*
* 标准 Fetch API Response 对象
*
* @returns Promise Response 对象
*
* @example
* ```typescript
* const response = await client.get('/api/users').response();
* console.log(response.status); // 200
* console.log(response.statusText); // 'OK'
* console.log(response.headers.get('Content-Type'));
* console.log(response.ok); // true
* ```
*/
response(): Promise;
/**
* 判断请求是否成功
*
* 基于 HTTP 状态码:200-299 为成功
*
* @returns Promise 是否成功
*
* @example
* ```typescript
* const ok = await client.get('/api/users').ok();
* if (ok) {
* console.log('Request succeeded');
* }
* ```
*/
ok(): Promise;
/**
* 获取请求的状态码
*
* HTTP 状态码:200, 404, 500 等
*
* @returns Promise 状态码
*
* @example
* ```typescript
* const status = await client.get('/api/users').status();
* if (status === 200) {
* console.log('OK');
* } else if (status === 404) {
* console.log('Not Found');
* }
* ```
*/
status(): Promise;
/**
* 获取请求的状态文本
*
* HTTP 状态文本:'OK', 'Not Found', 'Internal Server Error' 等
*
* @returns Promise 状态文本
*
* @example
* ```typescript
* const statusText = await client.get('/api/users').statusText();
* console.log(statusText); // 'OK'
* ```
*/
statusText(): Promise;
/**
* 获取返回的数据大小(字节)
*
* 通过解析 Content-Length 头获取
* 如果响应头没有 Content-Length,返回 0
*
* @returns Promise 数据大小(字节)
*
* @example
* ```typescript
* const size = await client.get('/file.pdf').size();
* console.log(`File size: ${size} bytes`);
* ```
*/
size(): Promise;
/**
* 获取返回的响应 Headers
*
* 标准 Fetch API Headers 对象
*
* @returns Promise Headers 对象
*
* @example
* ```typescript
* const headers = await client.get('/api/users').headers();
* console.log(headers.get('Content-Type'));
* console.log(headers.get('Content-Length'));
* console.log(headers.get('Cache-Control'));
* ```
*/
headers(): Promise;
}
/**
* 请求钩子函数
*
* 用于在请求生命周期的特定节点执行自定义逻辑
* 支持同步和异步钩子
*
* @param request - Request 对象,可以修改其属性
*
* @example
* ```typescript
* // 同步钩子
* const syncHook: RequestHook = (request) => {
* console.log('Request to:', request.url);
* request.init.headers['X-Timestamp'] = Date.now();
* };
*
* // 异步钩子
* const asyncHook: RequestHook = async (request) => {
* const token = await getToken();
* request.init.headers['Authorization'] = `Bearer ${token}`;
* };
*
* client.on('request', asyncHook);
* ```
*/
export interface RequestHook {
(request: Request): void | Promise;
}
/**
* 进度钩子函数
*
* 用于监听客户端整体请求进度
*
* @param progress - 进度值,范围 0-1
*
* @example
* ```typescript
* const progressHook: ProgressHook = (progress) => {
* console.log(`Progress: ${(progress * 100).toFixed(1)}%`);
* };
*
* client.on('progress', progressHook);
* // Progress: 33.3%
* // Progress: 66.7%
* // Progress: 100.0%
* ```
*/
export interface ProgressHook {
(progress: number): void;
}
/**
* 客户端配置(ClientOptions)
*
* 创建 Client 实例时可配置的选项
* 这些选项会被所有请求继承
*
* @example
* ```typescript
* import akita from 'akita';
*
* const client = akita.create({
* apiRoot: 'https://api.example.com',
* init: {
* headers: {
* 'Authorization': 'Bearer token',
* 'Accept': 'application/json'
* }
* },
* onProgress: (progress) => {
* console.log(`Progress: ${(progress * 100).toFixed(1)}%`);
* }
* });
* ```
*/
export interface ClientOptions {
/**
* 请求的基础 URL
*
* 所有请求路径会自动拼接到 apiRoot 后面
* 自动处理路径分隔符,确保不会出现双斜杠或缺少斜杠
*
* @example
* ```typescript
* const client = akita.create({
* apiRoot: 'https://api.example.com/v1'
* });
*
* client.get('/users') // https://api.example.com/v1/users
* client.get('users') // https://api.example.com/v1/users
* ```
*/
apiRoot?: string;
/**
* fetch() 函数默认参数
*
* 这些参数会与每个请求的参数合并
* 请求级参数优先级更高
*
* @example
* ```typescript
* const client = akita.create({
* init: {
* headers: {
* 'Authorization': 'Bearer token',
* 'Accept': 'application/json'
* },
* timeout: 10000,
* compress: true
* }
* });
*
* // 所有请求都会带上这些配置
* // 但可以在单个请求中覆盖
* client.get('/users', { timeout: 5000 }); // 覆盖 timeout
* ```
*/
init?: RequestInit;
/**
* fetch 函数引用
*
* 默认自动识别环境:
* - 浏览器:window.fetch
* - Node.js:需要手动注入或使用 node.ts 入口
* - 微信小程序:使用自定义 fetch.ts
*
* @example
* ```typescript
* import fetch from 'node-fetch';
*
* const client = akita.create({
* fetch
* });
* ```
*/
fetch?: Function;
/**
* FormData 类引用
*
* 用于文件上传和表单数据
* 默认自动识别环境
*
* @example
* ```typescript
* import FormData from 'form-data';
*
* const client = akita.create({
* FormData
* });
* ```
*/
FormData?: typeof FormData;
/**
* qs 配置
*
* 用于序列化 query 参数
* 详见 qs 库文档:https://github.com/ljharb/qs
*
* @example
* ```typescript
* const client = akita.create({
* qsOptions: {
* arrayFormat: 'indices', // 添加数组索引
* encode: false, // 不编码
* skipNulls: true // 跳过 null 值
* }
* });
*
* client.get('/api', { query: { ids: [1, 2, 3], name: null } });
* // -> /api?ids[0]=1&ids[1]=2&ids[2]=3
* ```
*/
qsOptions?: IStringifyOptions;
/**
* 自定义响应解析函数
*
* 用于解析非 JSON 格式的响应(如 XML、纯文本等)
* 如果不提供此选项,默认使用 JSON.parse 解析响应
*
* @param text - 响应的原始文本内容
* @returns 解析后的数据对象
*
* @example
* ```typescript
* // 使用 XML 解析器
* import { parseString } from 'xml2js';
*
* const client = akita.create({
* parser: (text) => {
* let result;
* parseString(text, (err, res) => {
* if (err) throw err;
* result = res;
* });
* return result;
* }
* });
*
* const xmlData = await client.get('/api/xml-endpoint');
* ```
*
* @remarks
* - 解析器应该是同步函数
* - 解析器抛出的错误会被包装为 ParseError
* - 204 No Content 响应不会调用此解析器
*/
parser?: (text: string) => any;
/**
* 请求前对 Body 进行编码的前置钩子
*
* 在 Body 被编码为 JSON/FormData 之前执行
* 可用于修改或验证 Body 数据
*
* @example
* ```typescript
* const client = akita.create({
* onEncode: async (request) => {
* // 添加时间戳
* if (request.init.body) {
* request.init.body.timestamp = Date.now();
* }
* }
* });
* ```
*/
onEncode?: RequestHook | RequestHook[];
/**
* 请求前钩子
*
* 在请求发送前执行,可以修改请求配置
* 支持多个钩子,按顺序执行
*
* @example
* ```typescript
* const client = akita.create({
* onRequest: [
* async (request) => {
* // 自动添加认证 token
* const token = await getToken();
* request.init.headers['Authorization'] = `Bearer ${token}`;
* },
* (request) => {
* // 添加请求 ID
* request.init.headers['X-Request-ID'] = generateId();
* }
* ]
* });
* ```
*/
onRequest?: RequestHook | RequestHook[];
/**
* 请求响应后钩子
*
* 在接收到响应后、解析响应体之前执行
* 可用于处理错误响应、日志记录等
*
* @example
* ```typescript
* const client = akita.create({
* onResponse: async (request) => {
* console.log(`${request.init.method} ${request.url} -> ${request.res?.status}`);
*
* // 处理特定状态码
* if (request.res?.status === 401) {
* console.log('Token expired, redirecting to login');
* window.location.href = '/login';
* }
* }
* });
* ```
*/
onResponse?: RequestHook | RequestHook[];
/**
* 请求响应后对 Body 进行解码的前置钩子
*
* 在响应文本解析为 JSON 之后、返回数据之前执行
* 可用于数据转换、字段重命名等
*
* @example
* ```typescript
* const client = akita.create({
* onDecode: async (request) => {
* // 转换日期字段
* if (request.value?.createdAt) {
* request.value.createdAt = new Date(request.value.createdAt);
* }
*
* // 提取嵌套数据
* if (request.value?.data) {
* request.value = request.value.data;
* }
* }
* });
* ```
*/
onDecode?: RequestHook | RequestHook[];
/**
* 请求进度通知钩子
*
* 监听客户端所有请求的总体进度
* 进度值范围 0-1,基于请求步骤计算(编码、发送、接收)
*
* @example
* ```typescript
* const client = akita.create({
* onProgress: (progress) => {
* console.log(`Overall progress: ${(progress * 100).toFixed(1)}%`);
* }
* });
*
* // 并发多个请求
* Promise.all([
* client.get('/api/1'),
* client.get('/api/2'),
* client.get('/api/3')
* ]);
* // Progress: 33.3%
* // Progress: 66.7%
* // Progress: 100.0%
* ```
*/
onProgress?: ProgressHook | ProgressHook[];
}
/**
* Client 客户端接口
*
* HTTP 客户端实例,提供请求方法和配置管理
*
* @example
* ```typescript
* import akita from 'akita';
*
* // 使用默认客户端
* const users = await akita.get('/api/users');
*
* // 创建新客户端
* const client = akita.create({
* apiRoot: 'https://api.example.com',
* init: { headers: { 'Authorization': 'Bearer token' } }
* });
*
* // 解析共享客户端
* const sharedClient = akita.resolve('api');
* sharedClient.get('/users');
* sharedClient.get('/users'); // 使用同一个实例
* ```
*/
export interface Client {
/**
* 内部使用:已发送请求数量
* @internal
*/
_count: number;
/**
* 内部使用:请求进度
* @internal
*/
_progress: number;
/**
* 内部使用:活跃请求列表
* @internal
*/
_tasks: Request[];
/**
* 内部使用:进度更新函数
* @internal
*/
_updateProgress: () => void;
/**
* 内部使用:进度更新定时器
* @internal
*/
_updateProgressTimer?: any;
/**
* 客户端配置(只读)
*
* 通过 setOptions() 方法修改
*
* @example
* ```typescript
* const client = akita.create({ apiRoot: 'https://api.example.com' });
* console.log(client.options.apiRoot); // 'https://api.example.com'
* ```
*/
readonly options: ClientOptions;
/**
* 请求进度(只读)
*
* 范围 0-1,表示所有活跃请求的总体进度
* 仅在配置了 onProgress 时可用
*
* @example
* ```typescript
* const client = akita.create({ onProgress: console.log });
* console.log(client.progress); // 0.5
* ```
*/
readonly progress?: number;
/**
* 创建新的客户端实例
*
* 新实例继承当前客户端的配置,但配置是独立的
*
* @param options - 客户端配置选项
* @returns 新的 Client 实例
*
* @example
* ```typescript
* // 基础客户端
* const baseClient = akita.create({
* apiRoot: 'https://api.example.com',
* init: { headers: { 'Accept': 'application/json' } }
* });
*
* // 创建带认证的客户端
* const authClient = baseClient.create({
* init: { headers: { 'Authorization': 'Bearer token' } }
* });
*
* // 两个客户端配置独立
* console.log(baseClient.options.init.headers); // { Accept: 'application/json' }
* console.log(authClient.options.init.headers); // { Accept: 'application/json', Authorization: 'Bearer token' }
* ```
*/
create(options: ClientOptions): Client;
/**
* 找回一个已经被初始化的客户端实例
*
* 如果不存在指定 key 的实例,将创建一个新的
* 方便多个模块共享同一个客户端实例
*
* @param key - 实例的唯一标识符
* @returns Client 实例
*
* @example
* ```typescript
* // 在 user.js 中
* import akita from 'akita';
* const client = akita.resolve('api');
* client.setOptions({ apiRoot: 'https://api.example.com' });
*
* // 在 product.js 中,获取同一个实例
* import akita from 'akita';
* const client = akita.resolve('api');
* // 共享配置,已经设置了 apiRoot
*
* // 在 order.js 中,再次获取
* import akita from 'akita';
* const client = akita.resolve('api');
* // 还是同一个实例
* ```
*/
resolve(key: string): Client;
/**
* 设置客户端配置
*
* 新配置会被合并到现有配置中
* 不会覆盖整个 options 对象
*
* @param options - 要添加或更新的配置
*
* @example
* ```typescript
* const client = akita.create({
* apiRoot: 'https://api.example.com',
* init: { headers: { 'Accept': 'application/json' } }
* });
*
* // 添加新配置(合并)
* client.setOptions({
* init: { headers: { 'Authorization': 'Bearer token' } }
* });
*
* // headers 会合并
* console.log(client.options.init.headers);
* // { Accept: 'application/json', Authorization: 'Bearer token' }
* ```
*/
setOptions(options: ClientOptions): void;
/**
* 获取 FormData 类引用
*
* 按优先级返回:
* 1. client.options.FormData
* 2. window.FormData(浏览器)
* 3. global.FormData(Node.js)
*
* @returns FormData 类构造函数
*
* @example
* ```typescript
* const FormData = client.getFormDataClass();
* const form = new FormData();
* form.append('file', fileInput.files[0]);
* ```
*/
getFormDataClass(): any;
/**
* 对 body 数据进行编码
*
* 根据数据类型自动转换:
* - File/Blob/Uint8Array/ReadableStream -> 保持原样
* - 包含 File 的对象 -> FormData
* - 普通对象 -> 保持原样(后续会根据 Content-Type 编码)
*
* @param body - 要编码的 body 数据
* @returns 编码后的数据(FormData 或原对象)
*
* @example
* ```typescript
* // 自动检测文件并创建 FormData
* const body = { name: 'test', file: fileInput.files[0] };
* const encoded = client.createBody(body);
* // encoded 是 FormData 实例
*
* // 普通对象保持不变
* const body = { name: 'test', age: 30 };
* const encoded = client.createBody(body);
* // encoded 是 { name: 'test', age: 30 }
* ```
*/
createBody(body: any): any | FormData;
/**
* 发起一个请求
*
* 通用请求方法,支持所有 HTTP 方法和自定义配置
*
* @template T 响应数据类型
* @param path - 请求路径(相对于 apiRoot)
* @param init - 请求配置
* @param reducer - 数据处理器
* @returns Request Request 对象
*
* @example
* ```typescript
* // 基本 GET
* const users = await client.request('/users');
*
* // 自定义方法
* const data = await client.request('/data', { method: 'HEAD' });
*
* // 使用 Reducer
* const users = await client.request('/users', {}, (json) => json.data);
*
* // 完整配置
* const data = await client.request('/api/search', {
* method: 'POST',
* query: { q: 'javascript' },
* body: { filters: { type: 'post' } },
* headers: { 'X-Custom': 'value' }
* });
* ```
*/
request(path: string, init?: RequestInit, reducer?: Reducer): Request;
/**
* 发起一个 GET 请求
*
* @template T 响应数据类型
* @param path - 请求路径
* @param init - 请求配置
* @returns Request Request 对象
*
* @example
* ```typescript
* // 简单 GET
* const users = await client.get('/users');
*
* // 带查询参数
* const users = await client.get('/users', {
* query: { page: 1, limit: 10 }
* });
*
* // 带类型
* interface User { id: number; name: string; }
* const users = await client.get('/users');
* ```
*/
get(path: string, init?: RequestInit): Request;
/**
* 发起一个 POST 请求
*
* @template T 响应数据类型
* @param path - 请求路径
* @param init - 请求配置,通常包含 body
* @returns Request Request 对象
*
* @example
* ```typescript
* // 创建用户
* const user = await client.post('/users', {
* body: { name: 'John', email: 'john@example.com' }
* });
*
* // 上传文件
* const result = await client.post('/upload', {
* body: { file: fileInput.files[0] }
* });
* ```
*/
post(path: string, init?: RequestInit): Request;
/**
* 发起一个 PUT 请求
*
* @template T 响应数据类型
* @param path - 请求路径
* @param init - 请求配置,通常包含 body
* @returns Request Request 对象
*
* @example
* ```typescript
* // 更新用户
* const user = await client.put('/users/1', {
* body: { name: 'John Doe', email: 'john.doe@example.com' }
* });
* ```
*/
put(path: string, init?: RequestInit): Request;
/**
* 发起一个 PATCH 请求
*
* @template T 响应数据类型
* @param path - 请求路径
* @param init - 请求配置,通常包含 body
* @returns Request Request 对象
*
* @example
* ```typescript
* // 部分更新用户
* const user = await client.patch('/users/1', {
* body: { name: 'John Doe' }
* });
* ```
*/
patch(path: string, init?: RequestInit): Request;
/**
* 发起一个 DELETE 请求
*
* @param path - 请求路径
* @param init - 请求配置
* @returns Request 返回 null
*
* @example
* ```typescript
* // 删除用户
* await client.delete('/users/1');
*
* // 带查询参数
* await client.delete('/users', { query: { ids: '1,2,3' } });
* ```
*/
delete(path: string, init?: RequestInit): Request;
/**
* 监听事件
*
* 支持的事件:
* - encode: Body 编码前
* - request: 请求发送前
* - response: 接收到响应后
* - decode: 响应体解析后
* - progress: 请求进度更新
*
* @param event - 事件名称
* @param hook - 钩子函数
* @returns this 返回自身以支持链式调用
*
* @example
* ```typescript
* // 监听请求事件
* client.on('request', (req) => {
* console.log('Sending request to:', req.url);
* });
*
* // 监听响应事件
* client.on('response', (req) => {
* console.log('Response status:', req.res?.status);
* });
*
* // 监听进度事件
* client.on('progress', (progress) => {
* console.log(`Progress: ${(progress * 100).toFixed(1)}%`);
* });
*
* // 链式调用
* client
* .on('request', logRequest)
* .on('response', logResponse);
* ```
*/
on(event: 'encode' | 'request' | 'response' | 'decode', hook: RequestHook): Client;
on(event: 'progress', hook: ProgressHook): Client;
/**
* 取消监听事件
*
* 移除之前通过 on() 添加的钩子
*
* @param event - 事件名称
* @param hook - 要移除的钩子函数引用
* @returns this 返回自身以支持链式调用
*
* @example
* ```typescript
* const hook = (req) => console.log(req.url);
* client.on('request', hook);
*
* // ...
*
* client.off('request', hook);
* ```
*/
off(event: 'encode' | 'request' | 'response' | 'decode', hook: RequestHook): Client;
off(event: 'progress', hook: ProgressHook): Client;
}
/**
* 默认 Client 实例
*
* 直接使用这个实例进行请求
*
* @example
* ```typescript
* import akita from 'akita';
*
* // 使用默认实例
* const users = await akita.get('/api/users');
*
* // 配置默认实例
* akita.setOptions({
* apiRoot: 'https://api.example.com',
* init: { headers: { 'Authorization': 'Bearer token' } }
* });
* ```
*/
declare const akita: Client;
/**
* 错误类型
*/
export type ErrorType = 'network' | 'http' | 'parse' | 'server';
/**
* 网络错误子类型
*/
export type NetworkErrorType =
| 'dns_failed'
| 'timeout'
| 'cors'
| 'offline'
| 'connection_refused'
| 'connection_reset'
| 'network_unreachable'
| 'unknown';
/**
* Akita 统一错误类
*
* 封装所有 HTTP 请求错误,提供结构化的错误信息和类型守卫
*
* ## 错误类型(type 字段)
* - 'network': 网络层错误(DNS 失败、超时、连接拒绝等)
* - 'http': HTTP 状态码错误(4xx/5xx)
* - 'parse': JSON/text 解析失败
* - 'server': 应用层错误(响应体包含 error 字段)
*
* ## 使用示例
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* if (isNetworkError(error)) {
* console.log('Network failed:', error.networkType); // 'timeout', 'dns_failed', etc.
* } else if (isHTTPError(error)) {
* console.log('HTTP error:', error.status, error.statusText);
* } else if (isServerError(error)) {
* console.log('Server error:', error.code, error.message);
* } else if (isParseError(error)) {
* console.log('Parse error:', error.cause);
* }
* }
* ```
*/
export class AkitaError extends Error {
/**
* 固定值 AkitaError
*/
name: 'AkitaError';
/**
* 错误类型
*
* - 'network': 网络层错误(DNS 失败、超时、连接拒绝等)
* - 'http': HTTP 状态码错误(4xx/5xx)
* - 'parse': JSON/text 解析失败
* - 'server': 应用层错误(响应体包含 error 字段)
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* console.log(error.type); // 'network', 'http', 'parse', or 'server'
* }
* ```
*/
type: ErrorType;
/**
* 错误代码
*
* 具体的错误标识符,用于错误识别和处理
*
* 常见错误代码:
* - Network Error: 'NETWORK_TIMEOUT', 'DNS_FAILED', 'CONNECTION_REFUSED', 'NETWORK_UNREACHABLE'
* - HTTP Error: 'HTTP_400', 'HTTP_401', 'HTTP_403', 'HTTP_404', 'HTTP_500', etc.
* - Parse Error: 'PARSE_JSON_ERROR', 'PARSE_TEXT_ERROR'
* - Server Error: 服务器返回的 code 字段值
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* console.log(error.code); // 'HTTP_404', 'NETWORK_TIMEOUT', etc.
* }
* ```
*/
code: string;
/**
* 网络错误子类型
*
* 仅当 type='network' 时可用
*
* 可能的值:
* - 'dns_failed': DNS 解析失败
* - 'timeout': 请求超时
* - 'cors': CORS 跨域错误
* - 'offline': 离线
* - 'connection_refused': 连接被拒绝
* - 'connection_reset': 连接重置
* - 'network_unreachable': 网络不可达
* - 'unknown': 未知网络错误
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* if (error.type === 'network') {
* console.log('Network issue:', error.networkType);
* }
* }
* ```
*/
networkType?: NetworkErrorType;
/**
* HTTP 状态码
*
* 仅当 type='http' 时可用
*
* 常见状态码:
* - 400: Bad Request
* - 401: Unauthorized
* - 403: Forbidden
* - 404: Not Found
* - 500: Internal Server Error
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* if (error.type === 'http') {
* console.log(`Status: ${error.status}`);
* }
* }
* ```
*/
status?: number;
/**
* HTTP 状态文本
*
* 仅当 type='http' 时可用
*
* 常见状态文本:
* - 'Bad Request', 'Unauthorized', 'Forbidden', 'Not Found'
* - 'Internal Server Error', 'Service Unavailable'
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* if (error.type === 'http') {
* console.log(`${error.status} ${error.statusText}`);
* }
* }
* ```
*/
statusText?: string;
/**
* HTTP 请求方法
*
* 发生错误时使用的 HTTP 方法
*
* 可能的值:'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 等
*
* @example
* ```typescript
* try {
* await client.post('/api/data', { body: { name: 'test' } });
* } catch (error) {
* console.log(`Failed to ${error.method} ${error.url}`);
* }
* ```
*/
method?: string;
/**
* 请求 URL
*
* 发生错误时请求的完整 URL(包含 apiRoot 和查询参数)
*
* @example
* ```typescript
* try {
* await client.get('/api/users');
* } catch (error) {
* console.log(`Failed to fetch: ${error.url}`);
* }
* ```
*/
url?: string;
/**
* 原始错误对象
*
* 仅当 type='parse' 时可用,包含底层的解析错误
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* if (error.type === 'parse') {
* console.log('Parse failed:', error.cause);
* }
* }
* ```
*/
cause?: Error;
/**
* 错误发生的时间戳
*
* Unix 时间戳(毫秒)
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* console.log(`Error at ${new Date(error.timestamp).toISOString()}`);
* }
* ```
*/
timestamp?: number;
constructor(
message: string,
type: ErrorType,
code: string,
options?: {
networkType?: NetworkErrorType;
status?: number;
statusText?: string;
url?: string;
method?: string;
cause?: Error;
timestamp?: number;
}
);
}
/**
* 类型守卫:判断错误是否为 AkitaError 实例
*
* 使用 TypeScript 类型守卫功能,在运行时检查错误是否为 AkitaError
* 如果返回 true,TypeScript 会将 error 类型收窄为 AkitaError
*
* @param error - 待检查的错误对象
* @returns 是否为 AkitaError 实例
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* if (isAkitaError(error)) {
* // error 现在的类型是 AkitaError
* console.log(error.type, error.code);
* } else {
* // 普通 Error 或其他类型
* console.log('Unknown error:', error);
* }
* }
* ```
*/
export function isAkitaError(error: any): error is AkitaError;
/**
* 类型守卫:判断错误是否为网络错误(type='network')
*
* 检查错误是否为 AkitaError 且 type='network'
* 包括 DNS 失败、超时、连接拒绝等网络层问题
*
* @param error - 待检查的错误对象
* @returns 是否为网络错误
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* if (isNetworkError(error)) {
* // 处理网络错误
* console.log('Network issue:', error.networkType);
* switch (error.networkType) {
* case 'timeout':
* showToast('请求超时,请重试');
* break;
* case 'offline':
* showToast('网络未连接');
* break;
* case 'dns_failed':
* showToast('DNS 解析失败');
* break;
* default:
* showToast('网络错误');
* }
* }
* }
* ```
*/
export function isNetworkError(error: any): error is AkitaError;
/**
* 类型守卫:判断错误是否为 HTTP 错误(type='http')
*
* 检查错误是否为 AkitaError 且 type='http'
* 服务器返回了 4xx 或 5xx 状态码
*
* @param error - 待检查的错误对象
* @returns 是否为 HTTP 错误
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* if (isHTTPError(error)) {
* // 处理 HTTP 错误
* if (error.status === 401) {
* // 未授权,跳转登录
* window.location.href = '/login';
* } else if (error.status === 404) {
* showToast('资源不存在');
* } else if (error.status >= 500) {
* showToast('服务器错误');
* }
* }
* }
* ```
*/
export function isHTTPError(error: any): error is AkitaError;
/**
* 类型守卫:判断错误是否为解析错误(type='parse')
*
* 检查错误是否为 AkitaError 且 type='parse'
* JSON 或文本解析失败
*
* @param error - 待检查的错误对象
* @returns 是否为解析错误
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* if (isParseError(error)) {
* // 处理解析错误
* console.error('Failed to parse response:', error.cause);
* showToast('数据格式错误');
* }
* }
* ```
*/
export function isParseError(error: any): error is AkitaError;
/**
* 类型守卫:判断错误是否为服务器错误(type='server')
*
* 检查错误是否为 AkitaError 且 type='server'
* 服务器返回的响应体包含 error 字段(值非 '0'/'null'/'none')
*
* @param error - 待检查的错误对象
* @returns 是否为服务器错误
*
* @example
* ```typescript
* try {
* await client.get('/api/data');
* } catch (error) {
* if (isServerError(error)) {
* // 处理服务器业务逻辑错误
* console.log('Server error code:', error.code);
* console.log('Server error message:', error.message);
* showToast(error.message || '操作失败');
* }
* }
* ```
*/
export function isServerError(error: any): error is AkitaError;
export default akita;