stream-axios 1.0.1 → 1.2.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/README.md +63 -6
- package/README.zh-CN.md +203 -149
- package/dist/index.cjs +90 -67
- package/dist/index.js +90 -64
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -22,7 +22,9 @@ npm install stream-axios
|
|
|
22
22
|
### 1. Basic Request (Same as axios)
|
|
23
23
|
|
|
24
24
|
```javascript
|
|
25
|
-
import
|
|
25
|
+
import { createInstance } from "stream-axios";
|
|
26
|
+
|
|
27
|
+
const request = createInstance();
|
|
26
28
|
|
|
27
29
|
// GET request
|
|
28
30
|
request
|
|
@@ -53,9 +55,11 @@ request
|
|
|
53
55
|
Suitable for scenarios like receiving large files or AI conversation streams.
|
|
54
56
|
|
|
55
57
|
```javascript
|
|
56
|
-
import
|
|
58
|
+
import { createInstance } from "stream-axios";
|
|
59
|
+
|
|
60
|
+
const request = createInstance();
|
|
57
61
|
|
|
58
|
-
const cancel = request.stream(
|
|
62
|
+
const cancel = await request.stream(
|
|
59
63
|
{
|
|
60
64
|
url: "/api/chat",
|
|
61
65
|
method: "POST",
|
|
@@ -106,17 +110,70 @@ myRequest.stream({ url: "/stream" }, (chunk) => console.log(chunk));
|
|
|
106
110
|
If you are handling SSE (Server-Sent Events) format data:
|
|
107
111
|
|
|
108
112
|
```javascript
|
|
109
|
-
import
|
|
113
|
+
import { createInstance, parseSSEChunk } from "stream-axios";
|
|
110
114
|
|
|
115
|
+
const request = createInstance();
|
|
116
|
+
|
|
117
|
+
// Simple usage - parse data field only
|
|
111
118
|
request.stream({ url: "/sse-endpoint", method: "GET" }, (chunk) => {
|
|
112
|
-
// Parse SSE data
|
|
113
119
|
parseSSEChunk(chunk, (content) => {
|
|
114
120
|
console.log("SSE Message:", content);
|
|
115
121
|
});
|
|
116
122
|
});
|
|
117
123
|
```
|
|
118
124
|
|
|
119
|
-
|
|
125
|
+
For production environments, use `createSSEParser` which handles chunks that may be split across multiple packets:
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
import { createInstance, createSSEParser } from "stream-axios";
|
|
129
|
+
|
|
130
|
+
const request = createInstance();
|
|
131
|
+
|
|
132
|
+
// Create a stateful parser with buffer
|
|
133
|
+
const parser = createSSEParser((event) => {
|
|
134
|
+
// event object contains: { event?, data?, id?, retry? }
|
|
135
|
+
console.log("Event type:", event.event);
|
|
136
|
+
console.log("Data:", event.data);
|
|
137
|
+
console.log("ID:", event.id);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
request.stream(
|
|
141
|
+
{ url: "/sse-endpoint", method: "GET" },
|
|
142
|
+
parser,
|
|
143
|
+
() => console.log("Completed"),
|
|
144
|
+
(error) => console.error("Error:", error),
|
|
145
|
+
);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 5. Cancel with External AbortSignal
|
|
149
|
+
|
|
150
|
+
You can pass an external `AbortSignal` to control the stream request:
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
import { createInstance } from "stream-axios";
|
|
154
|
+
|
|
155
|
+
const request = createInstance();
|
|
156
|
+
const controller = new AbortController();
|
|
157
|
+
|
|
158
|
+
request.stream(
|
|
159
|
+
{
|
|
160
|
+
url: "/api/chat",
|
|
161
|
+
method: "POST",
|
|
162
|
+
data: { message: "Hello" },
|
|
163
|
+
signal: controller.signal, // Pass external signal
|
|
164
|
+
},
|
|
165
|
+
(chunk) => console.log(chunk),
|
|
166
|
+
() => console.log("Completed"),
|
|
167
|
+
(error) => console.error(error),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Cancel from external controller
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
controller.abort();
|
|
173
|
+
}, 5000);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 6. Use with Existing Axios Instance
|
|
120
177
|
|
|
121
178
|
If you already have a configured axios instance in your project, you can attach the stream method to it:
|
|
122
179
|
|
package/README.zh-CN.md
CHANGED
|
@@ -1,149 +1,203 @@
|
|
|
1
|
-
# stream-axios
|
|
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 stream-axios
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## 使用指南
|
|
21
|
-
|
|
22
|
-
### 1. 基础请求 (同 axios)
|
|
23
|
-
|
|
24
|
-
```javascript
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
console.log(
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
},
|
|
66
|
-
() => {
|
|
67
|
-
//
|
|
68
|
-
console.log(
|
|
69
|
-
},
|
|
70
|
-
(
|
|
71
|
-
//
|
|
72
|
-
console.
|
|
73
|
-
}
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
import {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
1
|
+
# stream-axios
|
|
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 stream-axios
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 使用指南
|
|
21
|
+
|
|
22
|
+
### 1. 基础请求 (同 axios)
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
import { createInstance } from "stream-axios";
|
|
26
|
+
|
|
27
|
+
const request = createInstance();
|
|
28
|
+
|
|
29
|
+
// GET 请求
|
|
30
|
+
request
|
|
31
|
+
.get("/user?ID=12345")
|
|
32
|
+
.then(function (response) {
|
|
33
|
+
console.log(response);
|
|
34
|
+
})
|
|
35
|
+
.catch(function (error) {
|
|
36
|
+
console.log(error);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// POST 请求
|
|
40
|
+
request
|
|
41
|
+
.post("/user", {
|
|
42
|
+
firstName: "Fred",
|
|
43
|
+
lastName: "Flintstone",
|
|
44
|
+
})
|
|
45
|
+
.then(function (response) {
|
|
46
|
+
console.log(response);
|
|
47
|
+
})
|
|
48
|
+
.catch(function (error) {
|
|
49
|
+
console.log(error);
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. 流式请求 (Streaming)
|
|
54
|
+
|
|
55
|
+
适用于接收大文件或 AI 对话流等场景。
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
import request from "stream-axios";
|
|
59
|
+
|
|
60
|
+
const cancel = await request.stream(
|
|
61
|
+
{
|
|
62
|
+
url: "/api/chat",
|
|
63
|
+
method: "POST",
|
|
64
|
+
data: { message: "Hello" },
|
|
65
|
+
},
|
|
66
|
+
(chunk) => {
|
|
67
|
+
// 收到数据片段
|
|
68
|
+
console.log("Received chunk:", chunk);
|
|
69
|
+
},
|
|
70
|
+
() => {
|
|
71
|
+
// 请求完成
|
|
72
|
+
console.log("Stream completed");
|
|
73
|
+
},
|
|
74
|
+
(error) => {
|
|
75
|
+
// 发生错误
|
|
76
|
+
console.error("Stream error:", error);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// 如果需要取消请求
|
|
81
|
+
// cancel();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. 自定义实例
|
|
85
|
+
|
|
86
|
+
如果你需要独立的配置或拦截器:
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
import { createInstance } from "stream-axios";
|
|
90
|
+
|
|
91
|
+
const myRequest = createInstance({
|
|
92
|
+
baseURL: "https://api.mydomain.com",
|
|
93
|
+
timeout: 5000,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// 添加自定义拦截器
|
|
97
|
+
myRequest.interceptors.request.use((config) => {
|
|
98
|
+
config.headers["Authorization"] = "Bearer token";
|
|
99
|
+
return config;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// 使用流式方法
|
|
103
|
+
myRequest.stream({ url: "/stream" }, (chunk) => console.log(chunk));
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 4. SSE 解析助手
|
|
107
|
+
|
|
108
|
+
如果你处理的是 SSE (Server-Sent Events) 格式的数据:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
import { createInstance, parseSSEChunk } from "stream-axios";
|
|
112
|
+
|
|
113
|
+
const request = createInstance();
|
|
114
|
+
|
|
115
|
+
// 简单用法 - 仅解析 data 字段
|
|
116
|
+
request.stream({ url: "/sse-endpoint", method: "GET" }, (chunk) => {
|
|
117
|
+
parseSSEChunk(chunk, (content) => {
|
|
118
|
+
console.log("SSE Message:", content);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
对于生产环境,建议使用 `createSSEParser`,它可以处理跨数据包拆分的 chunk:
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
import { createInstance, createSSEParser } from "stream-axios";
|
|
127
|
+
|
|
128
|
+
const request = createInstance();
|
|
129
|
+
|
|
130
|
+
// 创建带缓冲区的有状态解析器
|
|
131
|
+
const parser = createSSEParser((event) => {
|
|
132
|
+
// event 对象包含: { event?, data?, id?, retry? }
|
|
133
|
+
console.log("事件类型:", event.event);
|
|
134
|
+
console.log("数据:", event.data);
|
|
135
|
+
console.log("ID:", event.id);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
request.stream(
|
|
139
|
+
{ url: "/sse-endpoint", method: "GET" },
|
|
140
|
+
parser,
|
|
141
|
+
() => console.log("完成"),
|
|
142
|
+
(error) => console.error("错误:", error),
|
|
143
|
+
);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 5. 使用外部 AbortSignal 取消请求
|
|
147
|
+
|
|
148
|
+
你可以传入外部的 `AbortSignal` 来控制流式请求:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
import { createInstance } from "stream-axios";
|
|
152
|
+
|
|
153
|
+
const request = createInstance();
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
|
|
156
|
+
request.stream(
|
|
157
|
+
{
|
|
158
|
+
url: "/api/chat",
|
|
159
|
+
method: "POST",
|
|
160
|
+
data: { message: "Hello" },
|
|
161
|
+
signal: controller.signal, // 传入外部信号
|
|
162
|
+
},
|
|
163
|
+
(chunk) => console.log(chunk),
|
|
164
|
+
() => console.log("完成"),
|
|
165
|
+
(error) => console.error(error),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// 从外部控制器取消请求
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
controller.abort();
|
|
171
|
+
}, 5000);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 6. 使用现有的 Axios 实例
|
|
175
|
+
|
|
176
|
+
如果你项目中已经有了配置好的 axios 实例,你可以将 stream 方法挂载到该实例上:
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
import axios from "axios";
|
|
180
|
+
import { attachStream } from "stream-axios";
|
|
181
|
+
|
|
182
|
+
// 你现有的 axios 实例
|
|
183
|
+
const myAxios = axios.create({
|
|
184
|
+
baseURL: "https://api.myproject.com",
|
|
185
|
+
headers: { "X-Custom-Header": "foobar" },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// 挂载 stream 方法
|
|
189
|
+
attachStream(myAxios);
|
|
190
|
+
|
|
191
|
+
// 现在你可以在实例上使用 .stream() 方法了
|
|
192
|
+
myAxios.stream({ url: "/chat" }, (chunk) => {
|
|
193
|
+
console.log(chunk);
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT
|
|
200
|
+
|
|
201
|
+
## 致谢
|
|
202
|
+
|
|
203
|
+
本项目基于 [Axios](https://github.com/axios/axios) 开发。
|
package/dist/index.cjs
CHANGED
|
@@ -1,79 +1,88 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
-
|
|
5
3
|
var axios = require('axios');
|
|
6
4
|
|
|
7
5
|
const defaultConfig = {
|
|
8
|
-
timeout:
|
|
6
|
+
timeout: 1000 * 15,
|
|
9
7
|
headers: {
|
|
10
|
-
"Content-Type": "application/json",
|
|
8
|
+
"Content-Type": "application/json;charset=utf-8",
|
|
11
9
|
},
|
|
12
10
|
};
|
|
13
11
|
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
-
* @param {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
};
|
|
12
|
+
/**
|
|
13
|
+
* Parse a single SSE event block
|
|
14
|
+
* @param {string} eventBlock - A single SSE event (lines separated by \n)
|
|
15
|
+
* @returns {{ event?: string, data?: string, id?: string, retry?: number } | null}
|
|
16
|
+
*/
|
|
17
|
+
function parseSSEEvent(eventBlock) {
|
|
18
|
+
const lines = eventBlock.split("\n");
|
|
19
|
+
const result = {};
|
|
20
|
+
const dataLines = [];
|
|
21
|
+
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
if (line.startsWith("data:")) {
|
|
24
|
+
// Support both "data: content" and "data:content"
|
|
25
|
+
const content = line.slice(5);
|
|
26
|
+
dataLines.push(content.startsWith(" ") ? content.slice(1) : content);
|
|
27
|
+
} else if (line.startsWith("event:")) {
|
|
28
|
+
result.event = line.slice(6).trim();
|
|
29
|
+
} else if (line.startsWith("id:")) {
|
|
30
|
+
result.id = line.slice(3).trim();
|
|
31
|
+
} else if (line.startsWith("retry:")) {
|
|
32
|
+
const retry = parseInt(line.slice(6).trim(), 10);
|
|
33
|
+
if (!isNaN(retry)) result.retry = retry;
|
|
34
|
+
}
|
|
35
|
+
// Ignore comments (lines starting with :) and unknown fields
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Join multiple data lines with newline (per SSE spec)
|
|
39
|
+
if (dataLines.length > 0) {
|
|
40
|
+
result.data = dataLines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
44
|
+
}
|
|
58
45
|
|
|
59
46
|
/**
|
|
60
|
-
* Parse SSE data chunk
|
|
61
|
-
* @param {string} sseText
|
|
62
|
-
* @param {Function} onMessage
|
|
47
|
+
* Parse SSE data chunk (simple, stateless version)
|
|
48
|
+
* @param {string} sseText - Raw SSE text chunk
|
|
49
|
+
* @param {Function} onMessage - Callback receiving data content string
|
|
63
50
|
*/
|
|
64
51
|
function parseSSEChunk(sseText, onMessage) {
|
|
65
|
-
const
|
|
66
|
-
for (const
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
if (validContent) {
|
|
71
|
-
onMessage(validContent);
|
|
72
|
-
}
|
|
52
|
+
const events = sseText.split("\n\n").filter(Boolean);
|
|
53
|
+
for (const eventBlock of events) {
|
|
54
|
+
const parsed = parseSSEEvent(eventBlock);
|
|
55
|
+
if (parsed?.data) {
|
|
56
|
+
onMessage(parsed.data);
|
|
73
57
|
}
|
|
74
58
|
}
|
|
75
59
|
}
|
|
76
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Create a stateful SSE parser with buffer for handling chunks that may be split
|
|
63
|
+
* @param {Function} onMessage - Callback receiving parsed SSE event object { event?, data?, id?, retry? }
|
|
64
|
+
* @returns {Function} Parser function that accepts raw chunk string
|
|
65
|
+
*/
|
|
66
|
+
function createSSEParser(onMessage) {
|
|
67
|
+
let buffer = "";
|
|
68
|
+
|
|
69
|
+
return (chunk) => {
|
|
70
|
+
buffer += chunk;
|
|
71
|
+
// Split by double newline (SSE event separator)
|
|
72
|
+
const parts = buffer.split("\n\n");
|
|
73
|
+
// Keep the last incomplete part in buffer
|
|
74
|
+
buffer = parts.pop() || "";
|
|
75
|
+
|
|
76
|
+
for (const eventBlock of parts) {
|
|
77
|
+
if (!eventBlock.trim()) continue;
|
|
78
|
+
const parsed = parseSSEEvent(eventBlock);
|
|
79
|
+
if (parsed) {
|
|
80
|
+
onMessage(parsed);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
/**
|
|
78
87
|
* Create a stream request function bound to an axios instance
|
|
79
88
|
* @param {import('axios').AxiosInstance} instance
|
|
@@ -82,9 +91,28 @@ const createStreamRequest = (instance) => {
|
|
|
82
91
|
return async (options = {}, onChunk, onComplete, onError) => {
|
|
83
92
|
const controller = new AbortController();
|
|
84
93
|
let reader = null;
|
|
94
|
+
let externalSignalHandler = null;
|
|
95
|
+
|
|
96
|
+
// Handle external signal
|
|
97
|
+
if (options.signal) {
|
|
98
|
+
if (options.signal.aborted) {
|
|
99
|
+
controller.abort();
|
|
100
|
+
} else {
|
|
101
|
+
externalSignalHandler = () => controller.abort();
|
|
102
|
+
options.signal.addEventListener("abort", externalSignalHandler);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const cleanup = () => {
|
|
107
|
+
if (options.signal && externalSignalHandler) {
|
|
108
|
+
options.signal.removeEventListener("abort", externalSignalHandler);
|
|
109
|
+
externalSignalHandler = null;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
85
112
|
|
|
86
113
|
const cancelRequest = () => {
|
|
87
114
|
try {
|
|
115
|
+
cleanup();
|
|
88
116
|
controller.abort();
|
|
89
117
|
reader?.releaseLock();
|
|
90
118
|
if (onError) onError("Stream request cancelled manually");
|
|
@@ -124,6 +152,7 @@ const createStreamRequest = (instance) => {
|
|
|
124
152
|
try {
|
|
125
153
|
const { done, value } = await reader.read();
|
|
126
154
|
if (done) {
|
|
155
|
+
cleanup();
|
|
127
156
|
if (onComplete) onComplete();
|
|
128
157
|
return;
|
|
129
158
|
}
|
|
@@ -132,6 +161,7 @@ const createStreamRequest = (instance) => {
|
|
|
132
161
|
if (onChunk) onChunk(chunk);
|
|
133
162
|
await readStreamChunk();
|
|
134
163
|
} catch (err) {
|
|
164
|
+
cleanup();
|
|
135
165
|
if (err.name === "AbortError") return;
|
|
136
166
|
if (onError) onError(`Read stream failed: ${err.message}`);
|
|
137
167
|
}
|
|
@@ -161,18 +191,12 @@ const createInstance = (config = {}) => {
|
|
|
161
191
|
const finalConfig = { ...defaultConfig, ...config };
|
|
162
192
|
const instance = axios.create(finalConfig);
|
|
163
193
|
|
|
164
|
-
// Setup interceptors
|
|
165
|
-
setupInterceptors(instance);
|
|
166
|
-
|
|
167
194
|
// Attach stream method
|
|
168
195
|
instance.stream = createStreamRequest(instance);
|
|
169
196
|
|
|
170
197
|
return instance;
|
|
171
198
|
};
|
|
172
199
|
|
|
173
|
-
// Create a default instance
|
|
174
|
-
const service = createInstance();
|
|
175
|
-
|
|
176
200
|
/**
|
|
177
201
|
* Attach stream method to an existing axios instance
|
|
178
202
|
* @param {import('axios').AxiosInstance} instance
|
|
@@ -185,6 +209,5 @@ const attachStream = (instance) => {
|
|
|
185
209
|
|
|
186
210
|
exports.attachStream = attachStream;
|
|
187
211
|
exports.createInstance = createInstance;
|
|
188
|
-
exports.
|
|
189
|
-
exports.default = service;
|
|
212
|
+
exports.createSSEParser = createSSEParser;
|
|
190
213
|
exports.parseSSEChunk = parseSSEChunk;
|
package/dist/index.js
CHANGED
|
@@ -1,75 +1,86 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
|
|
3
3
|
const defaultConfig = {
|
|
4
|
-
timeout:
|
|
4
|
+
timeout: 1000 * 15,
|
|
5
5
|
headers: {
|
|
6
|
-
"Content-Type": "application/json",
|
|
6
|
+
"Content-Type": "application/json;charset=utf-8",
|
|
7
7
|
},
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
* @param {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
};
|
|
10
|
+
/**
|
|
11
|
+
* Parse a single SSE event block
|
|
12
|
+
* @param {string} eventBlock - A single SSE event (lines separated by \n)
|
|
13
|
+
* @returns {{ event?: string, data?: string, id?: string, retry?: number } | null}
|
|
14
|
+
*/
|
|
15
|
+
function parseSSEEvent(eventBlock) {
|
|
16
|
+
const lines = eventBlock.split("\n");
|
|
17
|
+
const result = {};
|
|
18
|
+
const dataLines = [];
|
|
19
|
+
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
if (line.startsWith("data:")) {
|
|
22
|
+
// Support both "data: content" and "data:content"
|
|
23
|
+
const content = line.slice(5);
|
|
24
|
+
dataLines.push(content.startsWith(" ") ? content.slice(1) : content);
|
|
25
|
+
} else if (line.startsWith("event:")) {
|
|
26
|
+
result.event = line.slice(6).trim();
|
|
27
|
+
} else if (line.startsWith("id:")) {
|
|
28
|
+
result.id = line.slice(3).trim();
|
|
29
|
+
} else if (line.startsWith("retry:")) {
|
|
30
|
+
const retry = parseInt(line.slice(6).trim(), 10);
|
|
31
|
+
if (!isNaN(retry)) result.retry = retry;
|
|
32
|
+
}
|
|
33
|
+
// Ignore comments (lines starting with :) and unknown fields
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Join multiple data lines with newline (per SSE spec)
|
|
37
|
+
if (dataLines.length > 0) {
|
|
38
|
+
result.data = dataLines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
42
|
+
}
|
|
54
43
|
|
|
55
44
|
/**
|
|
56
|
-
* Parse SSE data chunk
|
|
57
|
-
* @param {string} sseText
|
|
58
|
-
* @param {Function} onMessage
|
|
45
|
+
* Parse SSE data chunk (simple, stateless version)
|
|
46
|
+
* @param {string} sseText - Raw SSE text chunk
|
|
47
|
+
* @param {Function} onMessage - Callback receiving data content string
|
|
59
48
|
*/
|
|
60
49
|
function parseSSEChunk(sseText, onMessage) {
|
|
61
|
-
const
|
|
62
|
-
for (const
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
if (validContent) {
|
|
67
|
-
onMessage(validContent);
|
|
68
|
-
}
|
|
50
|
+
const events = sseText.split("\n\n").filter(Boolean);
|
|
51
|
+
for (const eventBlock of events) {
|
|
52
|
+
const parsed = parseSSEEvent(eventBlock);
|
|
53
|
+
if (parsed?.data) {
|
|
54
|
+
onMessage(parsed.data);
|
|
69
55
|
}
|
|
70
56
|
}
|
|
71
57
|
}
|
|
72
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Create a stateful SSE parser with buffer for handling chunks that may be split
|
|
61
|
+
* @param {Function} onMessage - Callback receiving parsed SSE event object { event?, data?, id?, retry? }
|
|
62
|
+
* @returns {Function} Parser function that accepts raw chunk string
|
|
63
|
+
*/
|
|
64
|
+
function createSSEParser(onMessage) {
|
|
65
|
+
let buffer = "";
|
|
66
|
+
|
|
67
|
+
return (chunk) => {
|
|
68
|
+
buffer += chunk;
|
|
69
|
+
// Split by double newline (SSE event separator)
|
|
70
|
+
const parts = buffer.split("\n\n");
|
|
71
|
+
// Keep the last incomplete part in buffer
|
|
72
|
+
buffer = parts.pop() || "";
|
|
73
|
+
|
|
74
|
+
for (const eventBlock of parts) {
|
|
75
|
+
if (!eventBlock.trim()) continue;
|
|
76
|
+
const parsed = parseSSEEvent(eventBlock);
|
|
77
|
+
if (parsed) {
|
|
78
|
+
onMessage(parsed);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
/**
|
|
74
85
|
* Create a stream request function bound to an axios instance
|
|
75
86
|
* @param {import('axios').AxiosInstance} instance
|
|
@@ -78,9 +89,28 @@ const createStreamRequest = (instance) => {
|
|
|
78
89
|
return async (options = {}, onChunk, onComplete, onError) => {
|
|
79
90
|
const controller = new AbortController();
|
|
80
91
|
let reader = null;
|
|
92
|
+
let externalSignalHandler = null;
|
|
93
|
+
|
|
94
|
+
// Handle external signal
|
|
95
|
+
if (options.signal) {
|
|
96
|
+
if (options.signal.aborted) {
|
|
97
|
+
controller.abort();
|
|
98
|
+
} else {
|
|
99
|
+
externalSignalHandler = () => controller.abort();
|
|
100
|
+
options.signal.addEventListener("abort", externalSignalHandler);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const cleanup = () => {
|
|
105
|
+
if (options.signal && externalSignalHandler) {
|
|
106
|
+
options.signal.removeEventListener("abort", externalSignalHandler);
|
|
107
|
+
externalSignalHandler = null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
81
110
|
|
|
82
111
|
const cancelRequest = () => {
|
|
83
112
|
try {
|
|
113
|
+
cleanup();
|
|
84
114
|
controller.abort();
|
|
85
115
|
reader?.releaseLock();
|
|
86
116
|
if (onError) onError("Stream request cancelled manually");
|
|
@@ -120,6 +150,7 @@ const createStreamRequest = (instance) => {
|
|
|
120
150
|
try {
|
|
121
151
|
const { done, value } = await reader.read();
|
|
122
152
|
if (done) {
|
|
153
|
+
cleanup();
|
|
123
154
|
if (onComplete) onComplete();
|
|
124
155
|
return;
|
|
125
156
|
}
|
|
@@ -128,6 +159,7 @@ const createStreamRequest = (instance) => {
|
|
|
128
159
|
if (onChunk) onChunk(chunk);
|
|
129
160
|
await readStreamChunk();
|
|
130
161
|
} catch (err) {
|
|
162
|
+
cleanup();
|
|
131
163
|
if (err.name === "AbortError") return;
|
|
132
164
|
if (onError) onError(`Read stream failed: ${err.message}`);
|
|
133
165
|
}
|
|
@@ -157,18 +189,12 @@ const createInstance = (config = {}) => {
|
|
|
157
189
|
const finalConfig = { ...defaultConfig, ...config };
|
|
158
190
|
const instance = axios.create(finalConfig);
|
|
159
191
|
|
|
160
|
-
// Setup interceptors
|
|
161
|
-
setupInterceptors(instance);
|
|
162
|
-
|
|
163
192
|
// Attach stream method
|
|
164
193
|
instance.stream = createStreamRequest(instance);
|
|
165
194
|
|
|
166
195
|
return instance;
|
|
167
196
|
};
|
|
168
197
|
|
|
169
|
-
// Create a default instance
|
|
170
|
-
const service = createInstance();
|
|
171
|
-
|
|
172
198
|
/**
|
|
173
199
|
* Attach stream method to an existing axios instance
|
|
174
200
|
* @param {import('axios').AxiosInstance} instance
|
|
@@ -179,4 +205,4 @@ const attachStream = (instance) => {
|
|
|
179
205
|
return instance;
|
|
180
206
|
};
|
|
181
207
|
|
|
182
|
-
export { attachStream, createInstance,
|
|
208
|
+
export { attachStream, createInstance, createSSEParser, parseSSEChunk };
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stream-axios",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "二次封装 axios,保留原始配置,提供流式接口,提升开发效率",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.js",
|
|
8
|
+
"types": "index.d.ts",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
10
11
|
"import": "./dist/index.js",
|