stream-axios 1.1.0 → 1.2.1
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 +59 -32
- package/README.zh-CN.md +60 -33
- package/dist/index.cjs +89 -18
- package/dist/index.js +89 -15
- 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
|
|
@@ -50,10 +52,12 @@ request
|
|
|
50
52
|
|
|
51
53
|
### 2. Streaming Request
|
|
52
54
|
|
|
53
|
-
Suitable for scenarios like receiving large files or AI conversation streams.
|
|
55
|
+
Suitable for scenarios like receiving large files or AI conversation streams. The `stream` method returns a **cancel function** that you can call to abort the request.
|
|
54
56
|
|
|
55
57
|
```javascript
|
|
56
|
-
import
|
|
58
|
+
import { createInstance } from "stream-axios";
|
|
59
|
+
|
|
60
|
+
const request = createInstance();
|
|
57
61
|
|
|
58
62
|
const cancel = await request.stream(
|
|
59
63
|
{
|
|
@@ -75,13 +79,26 @@ const cancel = await request.stream(
|
|
|
75
79
|
},
|
|
76
80
|
);
|
|
77
81
|
|
|
78
|
-
//
|
|
79
|
-
|
|
82
|
+
// Cancel the stream manually when needed
|
|
83
|
+
cancel();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Optional: use `AbortSignal`** to cancel from outside (e.g. React cleanup):
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const controller = new AbortController();
|
|
90
|
+
await request.stream(
|
|
91
|
+
{ url: "/api/chat", signal: controller.signal },
|
|
92
|
+
onChunk,
|
|
93
|
+
onComplete,
|
|
94
|
+
onError,
|
|
95
|
+
);
|
|
96
|
+
// controller.abort(); // cancels the request
|
|
80
97
|
```
|
|
81
98
|
|
|
82
99
|
### 3. Custom Instance
|
|
83
100
|
|
|
84
|
-
|
|
101
|
+
`createInstance` merges your config with the default (timeout 15s, `Content-Type: application/json;charset=utf-8`). Override as needed:
|
|
85
102
|
|
|
86
103
|
```javascript
|
|
87
104
|
import { createInstance } from "stream-axios";
|
|
@@ -96,49 +113,59 @@ myRequest.interceptors.request.use((config) => {
|
|
|
96
113
|
config.headers["Authorization"] = "Bearer token";
|
|
97
114
|
return config;
|
|
98
115
|
});
|
|
99
|
-
|
|
100
|
-
// Use stream method
|
|
101
|
-
myRequest.stream({ url: "/stream" }, (chunk) => console.log(chunk));
|
|
102
116
|
```
|
|
103
117
|
|
|
104
|
-
### 4.
|
|
118
|
+
### 4. Attach Stream to Existing Axios Instance
|
|
105
119
|
|
|
106
|
-
If you
|
|
120
|
+
If you already have an axios instance, use `attachStream` to add the `stream` method without creating a new instance:
|
|
107
121
|
|
|
108
122
|
```javascript
|
|
109
|
-
import
|
|
123
|
+
import axios from "axios";
|
|
124
|
+
import { attachStream } from "stream-axios";
|
|
110
125
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
});
|
|
126
|
+
const instance = axios.create({ baseURL: "https://api.example.com" });
|
|
127
|
+
attachStream(instance);
|
|
128
|
+
|
|
129
|
+
// instance.stream() is now available
|
|
130
|
+
const cancel = await instance.stream({ url: "/api/stream" }, onChunk, onComplete, onError);
|
|
117
131
|
```
|
|
118
132
|
|
|
119
|
-
### 5.
|
|
133
|
+
### 5. Helper Functions
|
|
120
134
|
|
|
121
|
-
|
|
135
|
+
#### `createSSEParser` (stateful, handles split chunks)
|
|
136
|
+
|
|
137
|
+
Use for robust SSE parsing when chunks may be split across reads. Callback receives the full event object:
|
|
122
138
|
|
|
123
139
|
```javascript
|
|
124
|
-
import
|
|
125
|
-
|
|
140
|
+
import { createInstance, createSSEParser } from "stream-axios";
|
|
141
|
+
|
|
142
|
+
const request = createInstance();
|
|
126
143
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
const parser = createSSEParser((event) => {
|
|
145
|
+
// event: { event?: string, data?: string, id?: string, retry?: number }
|
|
146
|
+
if (event.data) {
|
|
147
|
+
console.log("SSE Data:", event.data);
|
|
148
|
+
}
|
|
131
149
|
});
|
|
132
150
|
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
await request.stream(
|
|
152
|
+
{ url: "/api/sse-stream" },
|
|
153
|
+
(chunk) => parser(chunk),
|
|
154
|
+
);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### `parseSSEChunk` (stateless, full chunks only)
|
|
158
|
+
|
|
159
|
+
Use when you have a complete SSE text chunk and only need the data content. Callback receives each message's data string:
|
|
135
160
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
161
|
+
```javascript
|
|
162
|
+
import { parseSSEChunk } from "stream-axios";
|
|
163
|
+
|
|
164
|
+
const sseText = "data: hello\n\ndata: world\n\n";
|
|
165
|
+
parseSSEChunk(sseText, (data) => {
|
|
166
|
+
console.log("Message:", data); // "hello", then "world"
|
|
139
167
|
});
|
|
140
168
|
```
|
|
141
|
-
|
|
142
169
|
## License
|
|
143
170
|
|
|
144
171
|
MIT
|
package/README.zh-CN.md
CHANGED
|
@@ -22,7 +22,9 @@ npm install stream-axios
|
|
|
22
22
|
### 1. 基础请求 (同 axios)
|
|
23
23
|
|
|
24
24
|
```javascript
|
|
25
|
-
import
|
|
25
|
+
import { createInstance } from "stream-axios";
|
|
26
|
+
|
|
27
|
+
const request = createInstance();
|
|
26
28
|
|
|
27
29
|
// GET 请求
|
|
28
30
|
request
|
|
@@ -50,10 +52,12 @@ request
|
|
|
50
52
|
|
|
51
53
|
### 2. 流式请求 (Streaming)
|
|
52
54
|
|
|
53
|
-
适用于接收大文件或 AI
|
|
55
|
+
适用于接收大文件或 AI 对话流等场景。`stream` 方法会返回一个**取消函数**,调用即可中止请求。
|
|
54
56
|
|
|
55
57
|
```javascript
|
|
56
|
-
import
|
|
58
|
+
import { createInstance } from "stream-axios";
|
|
59
|
+
|
|
60
|
+
const request = createInstance();
|
|
57
61
|
|
|
58
62
|
const cancel = await request.stream(
|
|
59
63
|
{
|
|
@@ -75,13 +79,26 @@ const cancel = await request.stream(
|
|
|
75
79
|
},
|
|
76
80
|
);
|
|
77
81
|
|
|
78
|
-
//
|
|
79
|
-
|
|
82
|
+
// 需要时手动取消流
|
|
83
|
+
cancel();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**可选:使用 `AbortSignal`** 从外部取消(例如 React 清理时):
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const controller = new AbortController();
|
|
90
|
+
await request.stream(
|
|
91
|
+
{ url: "/api/chat", signal: controller.signal },
|
|
92
|
+
onChunk,
|
|
93
|
+
onComplete,
|
|
94
|
+
onError,
|
|
95
|
+
);
|
|
96
|
+
// controller.abort(); // 取消请求
|
|
80
97
|
```
|
|
81
98
|
|
|
82
99
|
### 3. 自定义实例
|
|
83
100
|
|
|
84
|
-
|
|
101
|
+
`createInstance` 会将你的配置与默认配置(超时 15 秒、`Content-Type: application/json;charset=utf-8`)合并,可按需覆盖:
|
|
85
102
|
|
|
86
103
|
```javascript
|
|
87
104
|
import { createInstance } from "stream-axios";
|
|
@@ -96,53 +113,63 @@ myRequest.interceptors.request.use((config) => {
|
|
|
96
113
|
config.headers["Authorization"] = "Bearer token";
|
|
97
114
|
return config;
|
|
98
115
|
});
|
|
99
|
-
|
|
100
|
-
// 使用流式方法
|
|
101
|
-
myRequest.stream({ url: "/stream" }, (chunk) => console.log(chunk));
|
|
102
116
|
```
|
|
103
117
|
|
|
104
|
-
### 4.
|
|
118
|
+
### 4. 为已有 axios 实例挂载 stream
|
|
105
119
|
|
|
106
|
-
|
|
120
|
+
若已有 axios 实例,可用 `attachStream` 为其添加 `stream` 方法,无需新建实例:
|
|
107
121
|
|
|
108
122
|
```javascript
|
|
109
|
-
import
|
|
123
|
+
import axios from "axios";
|
|
124
|
+
import { attachStream } from "stream-axios";
|
|
110
125
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
});
|
|
126
|
+
const instance = axios.create({ baseURL: "https://api.example.com" });
|
|
127
|
+
attachStream(instance);
|
|
128
|
+
|
|
129
|
+
// instance.stream() 现已可用
|
|
130
|
+
const cancel = await instance.stream({ url: "/api/stream" }, onChunk, onComplete, onError);
|
|
117
131
|
```
|
|
118
132
|
|
|
119
|
-
### 5.
|
|
133
|
+
### 5. 辅助函数
|
|
120
134
|
|
|
121
|
-
|
|
135
|
+
#### `createSSEParser`(有状态,可处理分片)
|
|
136
|
+
|
|
137
|
+
当 SSE 数据可能被拆成多段时,用此解析器更稳妥。回调会收到完整事件对象:
|
|
122
138
|
|
|
123
139
|
```javascript
|
|
124
|
-
import
|
|
125
|
-
|
|
140
|
+
import { createInstance, createSSEParser } from "stream-axios";
|
|
141
|
+
|
|
142
|
+
const request = createInstance();
|
|
126
143
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
const parser = createSSEParser((event) => {
|
|
145
|
+
// event: { event?: string, data?: string, id?: string, retry?: number }
|
|
146
|
+
if (event.data) {
|
|
147
|
+
console.log("SSE Data:", event.data);
|
|
148
|
+
}
|
|
131
149
|
});
|
|
132
150
|
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
await request.stream(
|
|
152
|
+
{ url: "/api/sse-stream" },
|
|
153
|
+
(chunk) => parser(chunk),
|
|
154
|
+
);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### `parseSSEChunk`(无状态,仅完整块)
|
|
158
|
+
|
|
159
|
+
当已有完整的一段 SSE 文本且只需取出 data 内容时使用。回调仅接收每条消息的 data 字符串:
|
|
135
160
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
161
|
+
```javascript
|
|
162
|
+
import { parseSSEChunk } from "stream-axios";
|
|
163
|
+
|
|
164
|
+
const sseText = "data: hello\n\ndata: world\n\n";
|
|
165
|
+
parseSSEChunk(sseText, (data) => {
|
|
166
|
+
console.log("Message:", data); // "hello", 然后 "world"
|
|
139
167
|
});
|
|
140
168
|
```
|
|
141
|
-
|
|
142
169
|
## License
|
|
143
170
|
|
|
144
171
|
MIT
|
|
145
172
|
|
|
146
173
|
## 致谢
|
|
147
174
|
|
|
148
|
-
本项目基于 [Axios](https://github.com/axios/axios)
|
|
175
|
+
本项目基于 [Axios](https://github.com/axios/axios) 开发
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
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 = {
|
|
@@ -12,23 +10,79 @@ const defaultConfig = {
|
|
|
12
10
|
};
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
|
-
* Parse SSE
|
|
16
|
-
* @param {string}
|
|
17
|
-
* @
|
|
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
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
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
|
|
18
50
|
*/
|
|
19
51
|
function parseSSEChunk(sseText, onMessage) {
|
|
20
|
-
const
|
|
21
|
-
for (const
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
if (validContent) {
|
|
26
|
-
onMessage(validContent);
|
|
27
|
-
}
|
|
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);
|
|
28
57
|
}
|
|
29
58
|
}
|
|
30
59
|
}
|
|
31
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
|
+
|
|
32
86
|
/**
|
|
33
87
|
* Create a stream request function bound to an axios instance
|
|
34
88
|
* @param {import('axios').AxiosInstance} instance
|
|
@@ -37,9 +91,28 @@ const createStreamRequest = (instance) => {
|
|
|
37
91
|
return async (options = {}, onChunk, onComplete, onError) => {
|
|
38
92
|
const controller = new AbortController();
|
|
39
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
|
+
};
|
|
40
112
|
|
|
41
113
|
const cancelRequest = () => {
|
|
42
114
|
try {
|
|
115
|
+
cleanup();
|
|
43
116
|
controller.abort();
|
|
44
117
|
reader?.releaseLock();
|
|
45
118
|
if (onError) onError("Stream request cancelled manually");
|
|
@@ -79,6 +152,7 @@ const createStreamRequest = (instance) => {
|
|
|
79
152
|
try {
|
|
80
153
|
const { done, value } = await reader.read();
|
|
81
154
|
if (done) {
|
|
155
|
+
cleanup();
|
|
82
156
|
if (onComplete) onComplete();
|
|
83
157
|
return;
|
|
84
158
|
}
|
|
@@ -87,6 +161,7 @@ const createStreamRequest = (instance) => {
|
|
|
87
161
|
if (onChunk) onChunk(chunk);
|
|
88
162
|
await readStreamChunk();
|
|
89
163
|
} catch (err) {
|
|
164
|
+
cleanup();
|
|
90
165
|
if (err.name === "AbortError") return;
|
|
91
166
|
if (onError) onError(`Read stream failed: ${err.message}`);
|
|
92
167
|
}
|
|
@@ -122,9 +197,6 @@ const createInstance = (config = {}) => {
|
|
|
122
197
|
return instance;
|
|
123
198
|
};
|
|
124
199
|
|
|
125
|
-
// Create a default instance
|
|
126
|
-
const service = createInstance();
|
|
127
|
-
|
|
128
200
|
/**
|
|
129
201
|
* Attach stream method to an existing axios instance
|
|
130
202
|
* @param {import('axios').AxiosInstance} instance
|
|
@@ -137,6 +209,5 @@ const attachStream = (instance) => {
|
|
|
137
209
|
|
|
138
210
|
exports.attachStream = attachStream;
|
|
139
211
|
exports.createInstance = createInstance;
|
|
140
|
-
exports.
|
|
141
|
-
exports.default = service;
|
|
212
|
+
exports.createSSEParser = createSSEParser;
|
|
142
213
|
exports.parseSSEChunk = parseSSEChunk;
|
package/dist/index.js
CHANGED
|
@@ -8,23 +8,79 @@ const defaultConfig = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Parse SSE
|
|
12
|
-
* @param {string}
|
|
13
|
-
* @
|
|
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
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
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
|
|
14
48
|
*/
|
|
15
49
|
function parseSSEChunk(sseText, onMessage) {
|
|
16
|
-
const
|
|
17
|
-
for (const
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
if (validContent) {
|
|
22
|
-
onMessage(validContent);
|
|
23
|
-
}
|
|
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);
|
|
24
55
|
}
|
|
25
56
|
}
|
|
26
57
|
}
|
|
27
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
|
+
|
|
28
84
|
/**
|
|
29
85
|
* Create a stream request function bound to an axios instance
|
|
30
86
|
* @param {import('axios').AxiosInstance} instance
|
|
@@ -33,9 +89,28 @@ const createStreamRequest = (instance) => {
|
|
|
33
89
|
return async (options = {}, onChunk, onComplete, onError) => {
|
|
34
90
|
const controller = new AbortController();
|
|
35
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
|
+
};
|
|
36
110
|
|
|
37
111
|
const cancelRequest = () => {
|
|
38
112
|
try {
|
|
113
|
+
cleanup();
|
|
39
114
|
controller.abort();
|
|
40
115
|
reader?.releaseLock();
|
|
41
116
|
if (onError) onError("Stream request cancelled manually");
|
|
@@ -75,6 +150,7 @@ const createStreamRequest = (instance) => {
|
|
|
75
150
|
try {
|
|
76
151
|
const { done, value } = await reader.read();
|
|
77
152
|
if (done) {
|
|
153
|
+
cleanup();
|
|
78
154
|
if (onComplete) onComplete();
|
|
79
155
|
return;
|
|
80
156
|
}
|
|
@@ -83,6 +159,7 @@ const createStreamRequest = (instance) => {
|
|
|
83
159
|
if (onChunk) onChunk(chunk);
|
|
84
160
|
await readStreamChunk();
|
|
85
161
|
} catch (err) {
|
|
162
|
+
cleanup();
|
|
86
163
|
if (err.name === "AbortError") return;
|
|
87
164
|
if (onError) onError(`Read stream failed: ${err.message}`);
|
|
88
165
|
}
|
|
@@ -118,9 +195,6 @@ const createInstance = (config = {}) => {
|
|
|
118
195
|
return instance;
|
|
119
196
|
};
|
|
120
197
|
|
|
121
|
-
// Create a default instance
|
|
122
|
-
const service = createInstance();
|
|
123
|
-
|
|
124
198
|
/**
|
|
125
199
|
* Attach stream method to an existing axios instance
|
|
126
200
|
* @param {import('axios').AxiosInstance} instance
|
|
@@ -131,4 +205,4 @@ const attachStream = (instance) => {
|
|
|
131
205
|
return instance;
|
|
132
206
|
};
|
|
133
207
|
|
|
134
|
-
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.1
|
|
3
|
+
"version": "1.2.1",
|
|
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",
|