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 CHANGED
@@ -22,7 +22,9 @@ npm install stream-axios
22
22
  ### 1. Basic Request (Same as axios)
23
23
 
24
24
  ```javascript
25
- import request from "stream-axios";
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 request from "stream-axios";
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
- // If you need to cancel the request
79
- // cancel();
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
- If you need independent configuration or interceptors:
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. SSE Parsing Helper
118
+ ### 4. Attach Stream to Existing Axios Instance
105
119
 
106
- If you are handling SSE (Server-Sent Events) format data:
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 request, { parseSSEChunk } from "stream-axios";
123
+ import axios from "axios";
124
+ import { attachStream } from "stream-axios";
110
125
 
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
- });
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. Use with Existing Axios Instance
133
+ ### 5. Helper Functions
120
134
 
121
- If you already have a configured axios instance in your project, you can attach the stream method to it:
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 axios from "axios";
125
- import { attachStream } from "stream-axios";
140
+ import { createInstance, createSSEParser } from "stream-axios";
141
+
142
+ const request = createInstance();
126
143
 
127
- // Your existing axios instance
128
- const myAxios = axios.create({
129
- baseURL: "https://api.myproject.com",
130
- headers: { "X-Custom-Header": "foobar" },
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
- // Attach stream method
134
- attachStream(myAxios);
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
- // Now you can use .stream() on your instance
137
- myAxios.stream({ url: "/chat" }, (chunk) => {
138
- console.log(chunk);
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 request from "stream-axios";
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 request from "stream-axios";
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
- // cancel();
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. SSE 解析助手
118
+ ### 4. 为已有 axios 实例挂载 stream
105
119
 
106
- 如果你处理的是 SSE (Server-Sent Events) 格式的数据:
120
+ 若已有 axios 实例,可用 `attachStream` 为其添加 `stream` 方法,无需新建实例:
107
121
 
108
122
  ```javascript
109
- import request, { parseSSEChunk } from "stream-axios";
123
+ import axios from "axios";
124
+ import { attachStream } from "stream-axios";
110
125
 
111
- request.stream({ url: "/sse-endpoint", method: "GET" }, (chunk) => {
112
- // 解析 SSE 数据
113
- parseSSEChunk(chunk, (content) => {
114
- console.log("SSE Message:", content);
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. 使用现有的 Axios 实例
133
+ ### 5. 辅助函数
120
134
 
121
- 如果你项目中已经有了配置好的 axios 实例,你可以将 stream 方法挂载到该实例上:
135
+ #### `createSSEParser`(有状态,可处理分片)
136
+
137
+ 当 SSE 数据可能被拆成多段时,用此解析器更稳妥。回调会收到完整事件对象:
122
138
 
123
139
  ```javascript
124
- import axios from "axios";
125
- import { attachStream } from "stream-axios";
140
+ import { createInstance, createSSEParser } from "stream-axios";
141
+
142
+ const request = createInstance();
126
143
 
127
- // 你现有的 axios 实例
128
- const myAxios = axios.create({
129
- baseURL: "https://api.myproject.com",
130
- headers: { "X-Custom-Header": "foobar" },
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
- // 挂载 stream 方法
134
- attachStream(myAxios);
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
- // 现在你可以在实例上使用 .stream() 方法了
137
- myAxios.stream({ url: "/chat" }, (chunk) => {
138
- console.log(chunk);
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 data chunk
16
- * @param {string} sseText
17
- * @param {Function} onMessage
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 sseLines = sseText.split("\n\n").filter(Boolean);
21
- for (const line of sseLines) {
22
- const dataPrefix = "data: ";
23
- if (line.startsWith(dataPrefix)) {
24
- const validContent = line.slice(dataPrefix.length).trim();
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.createStreamRequest = createStreamRequest;
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 data chunk
12
- * @param {string} sseText
13
- * @param {Function} onMessage
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 sseLines = sseText.split("\n\n").filter(Boolean);
17
- for (const line of sseLines) {
18
- const dataPrefix = "data: ";
19
- if (line.startsWith(dataPrefix)) {
20
- const validContent = line.slice(dataPrefix.length).trim();
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, createStreamRequest, service as default, parseSSEChunk };
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.0",
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",