stream-axios 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/README.zh-CN.md +149 -0
- package/dist/index.cjs +190 -0
- package/dist/index.js +182 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 zovop
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# axios-stream
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | [简体中文](./README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
A wrapper library based on axios that retains the original configuration capabilities while providing out-of-the-box streaming request interfaces to improve development efficiency.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🚀 **Fully Compatible**: Based on axios, retaining all features like interceptors and configurations.
|
|
10
|
+
- 🌊 **Streaming Support**: Built-in `stream` method to easily handle streaming responses (e.g., LLM typewriter effect).
|
|
11
|
+
- 🛠 **Out of the Box**: Provides a default instance and supports creating custom instances.
|
|
12
|
+
- 📦 **SSE Helper**: Built-in SSE parsing tool for easy handling of Server-Sent Events.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install axios-stream
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage Guide
|
|
21
|
+
|
|
22
|
+
### 1. Basic Request (Same as axios)
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
import request from "axios-stream";
|
|
26
|
+
|
|
27
|
+
// GET request
|
|
28
|
+
request
|
|
29
|
+
.get("/user?ID=12345")
|
|
30
|
+
.then(function (response) {
|
|
31
|
+
console.log(response);
|
|
32
|
+
})
|
|
33
|
+
.catch(function (error) {
|
|
34
|
+
console.log(error);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// POST request
|
|
38
|
+
request
|
|
39
|
+
.post("/user", {
|
|
40
|
+
firstName: "Fred",
|
|
41
|
+
lastName: "Flintstone",
|
|
42
|
+
})
|
|
43
|
+
.then(function (response) {
|
|
44
|
+
console.log(response);
|
|
45
|
+
})
|
|
46
|
+
.catch(function (error) {
|
|
47
|
+
console.log(error);
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Streaming Request
|
|
52
|
+
|
|
53
|
+
Suitable for scenarios like receiving large files or AI conversation streams.
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
import request from "axios-stream";
|
|
57
|
+
|
|
58
|
+
const cancel = request.stream(
|
|
59
|
+
{
|
|
60
|
+
url: "/api/chat",
|
|
61
|
+
method: "POST",
|
|
62
|
+
data: { message: "Hello" },
|
|
63
|
+
},
|
|
64
|
+
(chunk) => {
|
|
65
|
+
// Received data chunk
|
|
66
|
+
console.log("Received chunk:", chunk);
|
|
67
|
+
},
|
|
68
|
+
() => {
|
|
69
|
+
// Stream completed
|
|
70
|
+
console.log("Stream completed");
|
|
71
|
+
},
|
|
72
|
+
(error) => {
|
|
73
|
+
// Error occurred
|
|
74
|
+
console.error("Stream error:", error);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// If you need to cancel the request
|
|
79
|
+
// cancel();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Custom Instance
|
|
83
|
+
|
|
84
|
+
If you need independent configuration or interceptors:
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
import { createInstance } from "axios-stream";
|
|
88
|
+
|
|
89
|
+
const myRequest = createInstance({
|
|
90
|
+
baseURL: "https://api.mydomain.com",
|
|
91
|
+
timeout: 5000,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Add custom interceptor
|
|
95
|
+
myRequest.interceptors.request.use((config) => {
|
|
96
|
+
config.headers["Authorization"] = "Bearer token";
|
|
97
|
+
return config;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Use stream method
|
|
101
|
+
myRequest.stream({ url: "/stream" }, (chunk) => console.log(chunk));
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 4. SSE Parsing Helper
|
|
105
|
+
|
|
106
|
+
If you are handling SSE (Server-Sent Events) format data:
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
import request, { parseSSEChunk } from "axios-stream";
|
|
110
|
+
|
|
111
|
+
request.stream({ url: "/sse-endpoint", method: "GET" }, (chunk) => {
|
|
112
|
+
// Parse SSE data
|
|
113
|
+
parseSSEChunk(chunk, (content) => {
|
|
114
|
+
console.log("SSE Message:", content);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 5. Use with Existing Axios Instance
|
|
120
|
+
|
|
121
|
+
If you already have a configured axios instance in your project, you can attach the stream method to it:
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
import axios from "axios";
|
|
125
|
+
import { attachStream } from "axios-stream";
|
|
126
|
+
|
|
127
|
+
// Your existing axios instance
|
|
128
|
+
const myAxios = axios.create({
|
|
129
|
+
baseURL: "https://api.myproject.com",
|
|
130
|
+
headers: { "X-Custom-Header": "foobar" },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Attach stream method
|
|
134
|
+
attachStream(myAxios);
|
|
135
|
+
|
|
136
|
+
// Now you can use .stream() on your instance
|
|
137
|
+
myAxios.stream({ url: "/chat" }, (chunk) => {
|
|
138
|
+
console.log(chunk);
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
145
|
+
|
|
146
|
+
## Acknowledgement
|
|
147
|
+
|
|
148
|
+
This project is based on [Axios](https://github.com/axios/axios).
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# axios-stream
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | [简体中文](./README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
这是一个基于 axios 的二次封装库,保留了 axios 的原始配置能力,并提供了开箱即用的流式请求(Streaming)接口,旨在提升开发效率。
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
- 🚀 **完全兼容**:基于 axios,保留原有拦截器、配置项等所有特性。
|
|
10
|
+
- 🌊 **流式支持**:内置 `stream` 方法,轻松处理流式响应(如 LLM 打字机效果)。
|
|
11
|
+
- 🛠 **开箱即用**:提供默认实例,也支持创建自定义实例。
|
|
12
|
+
- 📦 **SSE 助手**:内置 SSE 解析工具,方便处理 Server-Sent Events。
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install axios-stream
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 使用指南
|
|
21
|
+
|
|
22
|
+
### 1. 基础请求 (同 axios)
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
import request from 'axios-stream';
|
|
26
|
+
|
|
27
|
+
// GET 请求
|
|
28
|
+
request.get('/user?ID=12345')
|
|
29
|
+
.then(function (response) {
|
|
30
|
+
console.log(response);
|
|
31
|
+
})
|
|
32
|
+
.catch(function (error) {
|
|
33
|
+
console.log(error);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// POST 请求
|
|
37
|
+
request.post('/user', {
|
|
38
|
+
firstName: 'Fred',
|
|
39
|
+
lastName: 'Flintstone'
|
|
40
|
+
})
|
|
41
|
+
.then(function (response) {
|
|
42
|
+
console.log(response);
|
|
43
|
+
})
|
|
44
|
+
.catch(function (error) {
|
|
45
|
+
console.log(error);
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. 流式请求 (Streaming)
|
|
50
|
+
|
|
51
|
+
适用于接收大文件或 AI 对话流等场景。
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
import request from 'axios-stream';
|
|
55
|
+
|
|
56
|
+
const cancel = request.stream(
|
|
57
|
+
{
|
|
58
|
+
url: '/api/chat',
|
|
59
|
+
method: 'POST',
|
|
60
|
+
data: { message: 'Hello' }
|
|
61
|
+
},
|
|
62
|
+
(chunk) => {
|
|
63
|
+
// 收到数据片段
|
|
64
|
+
console.log('Received chunk:', chunk);
|
|
65
|
+
},
|
|
66
|
+
() => {
|
|
67
|
+
// 请求完成
|
|
68
|
+
console.log('Stream completed');
|
|
69
|
+
},
|
|
70
|
+
(error) => {
|
|
71
|
+
// 发生错误
|
|
72
|
+
console.error('Stream error:', error);
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// 如果需要取消请求
|
|
77
|
+
// cancel();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. 自定义实例
|
|
81
|
+
|
|
82
|
+
如果你需要独立的配置或拦截器:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
import { createInstance } from 'axios-stream';
|
|
86
|
+
|
|
87
|
+
const myRequest = createInstance({
|
|
88
|
+
baseURL: 'https://api.mydomain.com',
|
|
89
|
+
timeout: 5000
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 添加自定义拦截器
|
|
93
|
+
myRequest.interceptors.request.use(config => {
|
|
94
|
+
config.headers['Authorization'] = 'Bearer token';
|
|
95
|
+
return config;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// 使用流式方法
|
|
99
|
+
myRequest.stream({ url: '/stream' }, (chunk) => console.log(chunk));
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 4. SSE 解析助手
|
|
103
|
+
|
|
104
|
+
如果你处理的是 SSE (Server-Sent Events) 格式的数据:
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
import request, { parseSSEChunk } from 'axios-stream';
|
|
108
|
+
|
|
109
|
+
request.stream(
|
|
110
|
+
{ url: '/sse-endpoint', method: 'GET' },
|
|
111
|
+
(chunk) => {
|
|
112
|
+
// 解析 SSE 数据
|
|
113
|
+
parseSSEChunk(chunk, (content) => {
|
|
114
|
+
console.log('SSE Message:', content);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 5. 使用现有的 Axios 实例
|
|
121
|
+
|
|
122
|
+
如果你项目中已经有了配置好的 axios 实例,你可以将 stream 方法挂载到该实例上:
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
import axios from 'axios';
|
|
126
|
+
import { attachStream } from 'axios-stream';
|
|
127
|
+
|
|
128
|
+
// 你现有的 axios 实例
|
|
129
|
+
const myAxios = axios.create({
|
|
130
|
+
baseURL: 'https://api.myproject.com',
|
|
131
|
+
headers: { 'X-Custom-Header': 'foobar' }
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// 挂载 stream 方法
|
|
135
|
+
attachStream(myAxios);
|
|
136
|
+
|
|
137
|
+
// 现在你可以在实例上使用 .stream() 方法了
|
|
138
|
+
myAxios.stream({ url: '/chat' }, (chunk) => {
|
|
139
|
+
console.log(chunk);
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
|
146
|
+
|
|
147
|
+
## 致谢
|
|
148
|
+
|
|
149
|
+
本项目基于 [Axios](https://github.com/axios/axios) 开发。
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var axios = require('axios');
|
|
6
|
+
|
|
7
|
+
const defaultConfig = {
|
|
8
|
+
timeout: 10000,
|
|
9
|
+
headers: {
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Setup request interceptor
|
|
16
|
+
* @param {import('axios').AxiosInstance} instance
|
|
17
|
+
*/
|
|
18
|
+
const setupRequestInterceptor = (instance) => {
|
|
19
|
+
instance.interceptors.request.use(
|
|
20
|
+
(config) => {
|
|
21
|
+
// Add custom headers if needed
|
|
22
|
+
// config.headers["test"] = "testHEADER";
|
|
23
|
+
return config;
|
|
24
|
+
},
|
|
25
|
+
(error) => {
|
|
26
|
+
console.error("Request error:", error);
|
|
27
|
+
return Promise.reject(error);
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Setup response interceptor
|
|
34
|
+
* @param {import('axios').AxiosInstance} instance
|
|
35
|
+
*/
|
|
36
|
+
const setupResponseInterceptor = (instance) => {
|
|
37
|
+
instance.interceptors.response.use(
|
|
38
|
+
(response) => {
|
|
39
|
+
// If stream request, return response directly
|
|
40
|
+
if (response.config.isStream) {
|
|
41
|
+
return response;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Normal response handling
|
|
45
|
+
return response.data;
|
|
46
|
+
},
|
|
47
|
+
(error) => {
|
|
48
|
+
console.error("Response error:", error);
|
|
49
|
+
return Promise.reject(error);
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const setupInterceptors = (instance) => {
|
|
55
|
+
setupRequestInterceptor(instance);
|
|
56
|
+
setupResponseInterceptor(instance);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse SSE data chunk
|
|
61
|
+
* @param {string} sseText
|
|
62
|
+
* @param {Function} onMessage
|
|
63
|
+
*/
|
|
64
|
+
function parseSSEChunk(sseText, onMessage) {
|
|
65
|
+
const sseLines = sseText.split("\n\n").filter(Boolean);
|
|
66
|
+
for (const line of sseLines) {
|
|
67
|
+
const dataPrefix = "data: ";
|
|
68
|
+
if (line.startsWith(dataPrefix)) {
|
|
69
|
+
const validContent = line.slice(dataPrefix.length).trim();
|
|
70
|
+
if (validContent) {
|
|
71
|
+
onMessage(validContent);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a stream request function bound to an axios instance
|
|
79
|
+
* @param {import('axios').AxiosInstance} instance
|
|
80
|
+
*/
|
|
81
|
+
const createStreamRequest = (instance) => {
|
|
82
|
+
return async (options = {}, onChunk, onComplete, onError) => {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
let reader = null;
|
|
85
|
+
|
|
86
|
+
const cancelRequest = () => {
|
|
87
|
+
try {
|
|
88
|
+
controller.abort();
|
|
89
|
+
reader?.releaseLock();
|
|
90
|
+
if (onError) onError("Stream request cancelled manually");
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error("Failed to cancel stream request:", err);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const response = await instance({
|
|
98
|
+
...options,
|
|
99
|
+
isStream: true,
|
|
100
|
+
// Force critical config for streaming
|
|
101
|
+
adapter: "fetch",
|
|
102
|
+
responseType: "stream",
|
|
103
|
+
timeout: 0,
|
|
104
|
+
signal: controller.signal,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Handle both standard axios response and unwrapped response (by user interceptors)
|
|
108
|
+
// If user's interceptor returned response.data, 'response' itself might be the stream
|
|
109
|
+
const readableStream =
|
|
110
|
+
response.data ||
|
|
111
|
+
response.body ||
|
|
112
|
+
(response.getReader ? response : null);
|
|
113
|
+
|
|
114
|
+
if (!readableStream || typeof readableStream.getReader !== "function") {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"Browser does not support stream response, API did not return stream, or response was transformed incorrectly",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const decoder = new TextDecoder("utf-8");
|
|
121
|
+
reader = readableStream.getReader();
|
|
122
|
+
|
|
123
|
+
const readStreamChunk = async () => {
|
|
124
|
+
try {
|
|
125
|
+
const { done, value } = await reader.read();
|
|
126
|
+
if (done) {
|
|
127
|
+
if (onComplete) onComplete();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
131
|
+
|
|
132
|
+
if (onChunk) onChunk(chunk);
|
|
133
|
+
await readStreamChunk();
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (err.name === "AbortError") return;
|
|
136
|
+
if (onError) onError(`Read stream failed: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
readStreamChunk();
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (err.name === "CanceledError" || err.code === "ERR_CANCELED") {
|
|
143
|
+
// Handle axios cancellation if needed
|
|
144
|
+
return cancelRequest;
|
|
145
|
+
}
|
|
146
|
+
const errorMsg = err.message || "Stream request failed";
|
|
147
|
+
if (onError) onError(errorMsg);
|
|
148
|
+
console.error("Stream request failed:", errorMsg);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return cancelRequest;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create Axios instance with custom configuration
|
|
157
|
+
* @param {import('axios').AxiosRequestConfig} config
|
|
158
|
+
* @returns {import('axios').AxiosInstance}
|
|
159
|
+
*/
|
|
160
|
+
const createInstance = (config = {}) => {
|
|
161
|
+
const finalConfig = { ...defaultConfig, ...config };
|
|
162
|
+
const instance = axios.create(finalConfig);
|
|
163
|
+
|
|
164
|
+
// Setup interceptors
|
|
165
|
+
setupInterceptors(instance);
|
|
166
|
+
|
|
167
|
+
// Attach stream method
|
|
168
|
+
instance.stream = createStreamRequest(instance);
|
|
169
|
+
|
|
170
|
+
return instance;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Create a default instance
|
|
174
|
+
const service = createInstance();
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Attach stream method to an existing axios instance
|
|
178
|
+
* @param {import('axios').AxiosInstance} instance
|
|
179
|
+
* @returns {import('axios').AxiosInstance}
|
|
180
|
+
*/
|
|
181
|
+
const attachStream = (instance) => {
|
|
182
|
+
instance.stream = createStreamRequest(instance);
|
|
183
|
+
return instance;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
exports.attachStream = attachStream;
|
|
187
|
+
exports.createInstance = createInstance;
|
|
188
|
+
exports.createStreamRequest = createStreamRequest;
|
|
189
|
+
exports.default = service;
|
|
190
|
+
exports.parseSSEChunk = parseSSEChunk;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
const defaultConfig = {
|
|
4
|
+
timeout: 10000,
|
|
5
|
+
headers: {
|
|
6
|
+
"Content-Type": "application/json",
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Setup request interceptor
|
|
12
|
+
* @param {import('axios').AxiosInstance} instance
|
|
13
|
+
*/
|
|
14
|
+
const setupRequestInterceptor = (instance) => {
|
|
15
|
+
instance.interceptors.request.use(
|
|
16
|
+
(config) => {
|
|
17
|
+
// Add custom headers if needed
|
|
18
|
+
// config.headers["test"] = "testHEADER";
|
|
19
|
+
return config;
|
|
20
|
+
},
|
|
21
|
+
(error) => {
|
|
22
|
+
console.error("Request error:", error);
|
|
23
|
+
return Promise.reject(error);
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Setup response interceptor
|
|
30
|
+
* @param {import('axios').AxiosInstance} instance
|
|
31
|
+
*/
|
|
32
|
+
const setupResponseInterceptor = (instance) => {
|
|
33
|
+
instance.interceptors.response.use(
|
|
34
|
+
(response) => {
|
|
35
|
+
// If stream request, return response directly
|
|
36
|
+
if (response.config.isStream) {
|
|
37
|
+
return response;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Normal response handling
|
|
41
|
+
return response.data;
|
|
42
|
+
},
|
|
43
|
+
(error) => {
|
|
44
|
+
console.error("Response error:", error);
|
|
45
|
+
return Promise.reject(error);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const setupInterceptors = (instance) => {
|
|
51
|
+
setupRequestInterceptor(instance);
|
|
52
|
+
setupResponseInterceptor(instance);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse SSE data chunk
|
|
57
|
+
* @param {string} sseText
|
|
58
|
+
* @param {Function} onMessage
|
|
59
|
+
*/
|
|
60
|
+
function parseSSEChunk(sseText, onMessage) {
|
|
61
|
+
const sseLines = sseText.split("\n\n").filter(Boolean);
|
|
62
|
+
for (const line of sseLines) {
|
|
63
|
+
const dataPrefix = "data: ";
|
|
64
|
+
if (line.startsWith(dataPrefix)) {
|
|
65
|
+
const validContent = line.slice(dataPrefix.length).trim();
|
|
66
|
+
if (validContent) {
|
|
67
|
+
onMessage(validContent);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a stream request function bound to an axios instance
|
|
75
|
+
* @param {import('axios').AxiosInstance} instance
|
|
76
|
+
*/
|
|
77
|
+
const createStreamRequest = (instance) => {
|
|
78
|
+
return async (options = {}, onChunk, onComplete, onError) => {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
let reader = null;
|
|
81
|
+
|
|
82
|
+
const cancelRequest = () => {
|
|
83
|
+
try {
|
|
84
|
+
controller.abort();
|
|
85
|
+
reader?.releaseLock();
|
|
86
|
+
if (onError) onError("Stream request cancelled manually");
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error("Failed to cancel stream request:", err);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const response = await instance({
|
|
94
|
+
...options,
|
|
95
|
+
isStream: true,
|
|
96
|
+
// Force critical config for streaming
|
|
97
|
+
adapter: "fetch",
|
|
98
|
+
responseType: "stream",
|
|
99
|
+
timeout: 0,
|
|
100
|
+
signal: controller.signal,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Handle both standard axios response and unwrapped response (by user interceptors)
|
|
104
|
+
// If user's interceptor returned response.data, 'response' itself might be the stream
|
|
105
|
+
const readableStream =
|
|
106
|
+
response.data ||
|
|
107
|
+
response.body ||
|
|
108
|
+
(response.getReader ? response : null);
|
|
109
|
+
|
|
110
|
+
if (!readableStream || typeof readableStream.getReader !== "function") {
|
|
111
|
+
throw new Error(
|
|
112
|
+
"Browser does not support stream response, API did not return stream, or response was transformed incorrectly",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const decoder = new TextDecoder("utf-8");
|
|
117
|
+
reader = readableStream.getReader();
|
|
118
|
+
|
|
119
|
+
const readStreamChunk = async () => {
|
|
120
|
+
try {
|
|
121
|
+
const { done, value } = await reader.read();
|
|
122
|
+
if (done) {
|
|
123
|
+
if (onComplete) onComplete();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
127
|
+
|
|
128
|
+
if (onChunk) onChunk(chunk);
|
|
129
|
+
await readStreamChunk();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err.name === "AbortError") return;
|
|
132
|
+
if (onError) onError(`Read stream failed: ${err.message}`);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
readStreamChunk();
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (err.name === "CanceledError" || err.code === "ERR_CANCELED") {
|
|
139
|
+
// Handle axios cancellation if needed
|
|
140
|
+
return cancelRequest;
|
|
141
|
+
}
|
|
142
|
+
const errorMsg = err.message || "Stream request failed";
|
|
143
|
+
if (onError) onError(errorMsg);
|
|
144
|
+
console.error("Stream request failed:", errorMsg);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return cancelRequest;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create Axios instance with custom configuration
|
|
153
|
+
* @param {import('axios').AxiosRequestConfig} config
|
|
154
|
+
* @returns {import('axios').AxiosInstance}
|
|
155
|
+
*/
|
|
156
|
+
const createInstance = (config = {}) => {
|
|
157
|
+
const finalConfig = { ...defaultConfig, ...config };
|
|
158
|
+
const instance = axios.create(finalConfig);
|
|
159
|
+
|
|
160
|
+
// Setup interceptors
|
|
161
|
+
setupInterceptors(instance);
|
|
162
|
+
|
|
163
|
+
// Attach stream method
|
|
164
|
+
instance.stream = createStreamRequest(instance);
|
|
165
|
+
|
|
166
|
+
return instance;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Create a default instance
|
|
170
|
+
const service = createInstance();
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Attach stream method to an existing axios instance
|
|
174
|
+
* @param {import('axios').AxiosInstance} instance
|
|
175
|
+
* @returns {import('axios').AxiosInstance}
|
|
176
|
+
*/
|
|
177
|
+
const attachStream = (instance) => {
|
|
178
|
+
instance.stream = createStreamRequest(instance);
|
|
179
|
+
return instance;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export { attachStream, createInstance, createStreamRequest, service as default, parseSSEChunk };
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stream-axios",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "二次封装 axios,保留原始配置,提供流式接口,提升开发效率",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"README.zh-CN.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"axios",
|
|
22
|
+
"stream",
|
|
23
|
+
"fetch",
|
|
24
|
+
"sse",
|
|
25
|
+
"openai",
|
|
26
|
+
"llm",
|
|
27
|
+
"streaming"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "rollup -c",
|
|
34
|
+
"prepublishOnly": "npm run build",
|
|
35
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/zOvOp/axios-stream.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/zOvOp/axios-stream/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/zOvOp/axios-stream#readme",
|
|
45
|
+
"author": "zovop",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"axios": "^1.13.4"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
52
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
53
|
+
"rollup": "^4.57.1"
|
|
54
|
+
}
|
|
55
|
+
}
|