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 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,9 +55,11 @@ 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
- 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 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
@@ -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 request from 'stream-axios';
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 'stream-axios';
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 'stream-axios';
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 'stream-axios';
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 'stream-axios';
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) 开发。
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: 10000,
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
- * 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
- };
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 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
- }
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.createStreamRequest = createStreamRequest;
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: 10000,
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
- * 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
- };
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 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
- }
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, 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.0.1",
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",