stream-axios 1.1.0 → 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 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
@@ -53,7 +55,9 @@ request
53
55
  Suitable for scenarios like receiving large files or AI conversation streams.
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
  {
@@ -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 request, { parseSSEChunk } from "stream-axios";
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
- ### 5. Use with Existing Axios Instance
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
@@ -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
@@ -106,17 +108,70 @@ myRequest.stream({ url: "/stream" }, (chunk) => console.log(chunk));
106
108
  如果你处理的是 SSE (Server-Sent Events) 格式的数据:
107
109
 
108
110
  ```javascript
109
- import request, { parseSSEChunk } from "stream-axios";
111
+ import { createInstance, parseSSEChunk } from "stream-axios";
112
+
113
+ const request = createInstance();
110
114
 
115
+ // 简单用法 - 仅解析 data 字段
111
116
  request.stream({ url: "/sse-endpoint", method: "GET" }, (chunk) => {
112
- // 解析 SSE 数据
113
117
  parseSSEChunk(chunk, (content) => {
114
118
  console.log("SSE Message:", content);
115
119
  });
116
120
  });
117
121
  ```
118
122
 
119
- ### 5. 使用现有的 Axios 实例
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 实例
120
175
 
121
176
  如果你项目中已经有了配置好的 axios 实例,你可以将 stream 方法挂载到该实例上:
122
177
 
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.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",