@ahoo-wang/fetcher-eventstream 0.11.2 → 1.0.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 +167 -1
- package/README.zh-CN.md +142 -0
- package/dist/eventStreamInterceptor.d.ts.map +1 -1
- package/dist/index.es.js +3 -1
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/jsonServerSentEventTransformStream.d.ts.map +1 -1
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/@ahoo-wang/fetcher-eventstream)
|
|
9
9
|
[](https://deepwiki.com/Ahoo-Wang/fetcher)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Power your real-time applications with Server-Sent Events support, specially designed for Large Language Model streaming
|
|
12
|
+
APIs.
|
|
12
13
|
|
|
13
14
|
## 🌟 Features
|
|
14
15
|
|
|
@@ -38,6 +39,171 @@ pnpm add @ahoo-wang/fetcher-eventstream
|
|
|
38
39
|
yarn add @ahoo-wang/fetcher-eventstream
|
|
39
40
|
```
|
|
40
41
|
|
|
42
|
+
### Integration Test Example: LLM Client with Event Stream
|
|
43
|
+
|
|
44
|
+
The following example shows how to create an LLM client with event stream support, similar to the integration test in
|
|
45
|
+
the Fetcher project. You can find the complete implementation
|
|
46
|
+
in [integration-test/src/eventstream/llmClient.ts](../../integration-test/src/eventstream/llmClient.ts).
|
|
47
|
+
|
|
48
|
+
This example demonstrates how to interact with popular LLM APIs like OpenAI's GPT models using Fetcher's streaming
|
|
49
|
+
capabilities.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import {
|
|
53
|
+
BaseURLCapable,
|
|
54
|
+
ContentTypeValues,
|
|
55
|
+
FetchExchange,
|
|
56
|
+
NamedFetcher,
|
|
57
|
+
REQUEST_BODY_INTERCEPTOR_ORDER,
|
|
58
|
+
RequestInterceptor,
|
|
59
|
+
} from '@ahoo-wang/fetcher';
|
|
60
|
+
import {
|
|
61
|
+
api,
|
|
62
|
+
autoGeneratedError,
|
|
63
|
+
body,
|
|
64
|
+
post,
|
|
65
|
+
ResultExtractors,
|
|
66
|
+
} from '@ahoo-wang/fetcher-decorator';
|
|
67
|
+
import {
|
|
68
|
+
EventStreamInterceptor,
|
|
69
|
+
JsonServerSentEventStream,
|
|
70
|
+
} from '@ahoo-wang/fetcher-eventstream';
|
|
71
|
+
import { ChatRequest, ChatResponse } from './types';
|
|
72
|
+
|
|
73
|
+
export const llmFetcherName = 'llm';
|
|
74
|
+
|
|
75
|
+
export interface LlmOptions extends BaseURLCapable {
|
|
76
|
+
apiKey: string;
|
|
77
|
+
model?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class LlmRequestInterceptor implements RequestInterceptor {
|
|
81
|
+
readonly name: string = 'LlmRequestInterceptor';
|
|
82
|
+
readonly order: number = REQUEST_BODY_INTERCEPTOR_ORDER - 1;
|
|
83
|
+
|
|
84
|
+
constructor(private llmOptions: LlmOptions) {
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
intercept(exchange: FetchExchange): void {
|
|
88
|
+
const chatRequest = exchange.request.body as ChatRequest;
|
|
89
|
+
if (!chatRequest.model) {
|
|
90
|
+
chatRequest.model = this.llmOptions.model;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createLlmFetcher(options: LlmOptions): NamedFetcher {
|
|
96
|
+
const llmFetcher = new NamedFetcher(llmFetcherName, {
|
|
97
|
+
baseURL: options.baseURL,
|
|
98
|
+
headers: {
|
|
99
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
100
|
+
'Content-Type': ContentTypeValues.APPLICATION_JSON,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
llmFetcher.interceptors.request.use(new LlmRequestInterceptor(options));
|
|
104
|
+
llmFetcher.interceptors.response.use(new EventStreamInterceptor());
|
|
105
|
+
return llmFetcher;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@api('/chat', {
|
|
109
|
+
fetcher: llmFetcherName,
|
|
110
|
+
resultExtractor: ResultExtractors.JsonEventStream,
|
|
111
|
+
})
|
|
112
|
+
export class LlmClient {
|
|
113
|
+
@post('/completions')
|
|
114
|
+
streamChat(
|
|
115
|
+
@body() _body: ChatRequest,
|
|
116
|
+
): Promise<JsonServerSentEventStream<ChatResponse>> {
|
|
117
|
+
throw autoGeneratedError();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@post('/completions', { resultExtractor: ResultExtractors.Json })
|
|
121
|
+
chat(@body() _body: ChatRequest): Promise<ChatResponse> {
|
|
122
|
+
throw autoGeneratedError();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Using streamChat for Real-time Responses
|
|
128
|
+
|
|
129
|
+
Here's how to use the `streamChat` method to get real-time responses from an LLM API:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { createLlmFetcher, LlmClient } from './llmClient';
|
|
133
|
+
|
|
134
|
+
// Initialize the LLM client with your API configuration
|
|
135
|
+
const llmFetcher = createLlmFetcher({
|
|
136
|
+
baseURL: 'https://api.openai.com/v1', // Example for OpenAI
|
|
137
|
+
apiKey: process.env.OPENAI_API_KEY || 'your-api-key',
|
|
138
|
+
model: 'gpt-3.5-turbo', // Default model
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Create the client instance
|
|
142
|
+
const llmClient = new LlmClient();
|
|
143
|
+
|
|
144
|
+
// Example: Stream a chat completion response in real-time
|
|
145
|
+
async function streamChatExample() {
|
|
146
|
+
try {
|
|
147
|
+
// Stream the response token by token
|
|
148
|
+
const stream = await llmClient.streamChat({
|
|
149
|
+
messages: [
|
|
150
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
151
|
+
{ role: 'user', content: 'Explain quantum computing in simple terms.' },
|
|
152
|
+
],
|
|
153
|
+
model: 'gpt-3.5-turbo', // Override default model if needed
|
|
154
|
+
stream: true, // Enable streaming
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Process the streamed response
|
|
158
|
+
let fullResponse = '';
|
|
159
|
+
for await (const event of stream) {
|
|
160
|
+
// Each event contains a partial response
|
|
161
|
+
if (event.data) {
|
|
162
|
+
const chunk = event.data;
|
|
163
|
+
const content = chunk.choices[0]?.delta?.content || '';
|
|
164
|
+
fullResponse += content;
|
|
165
|
+
console.log('New token:', content);
|
|
166
|
+
|
|
167
|
+
// Update UI in real-time as tokens arrive
|
|
168
|
+
updateUI(content);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log('Full response:', fullResponse);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error('Error streaming chat:', error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Helper function to simulate UI updates
|
|
179
|
+
function updateUI(content: string) {
|
|
180
|
+
// In a real application, this would update your UI
|
|
181
|
+
process.stdout.write(content);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Manual Conversion
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { toServerSentEventStream } from '@ahoo-wang/fetcher-eventstream';
|
|
189
|
+
|
|
190
|
+
// Convert a Response object manually
|
|
191
|
+
const response = await fetch('/events');
|
|
192
|
+
const eventStream = toServerSentEventStream(response);
|
|
193
|
+
|
|
194
|
+
// Read events from the stream
|
|
195
|
+
const reader = eventStream.getReader();
|
|
196
|
+
try {
|
|
197
|
+
while (true) {
|
|
198
|
+
const { done, value } = await reader.read();
|
|
199
|
+
if (done) break;
|
|
200
|
+
console.log('Received event:', value);
|
|
201
|
+
}
|
|
202
|
+
} finally {
|
|
203
|
+
reader.releaseLock();
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
41
207
|
### Basic Usage with Interceptor
|
|
42
208
|
|
|
43
209
|
```typescript
|
package/README.zh-CN.md
CHANGED
|
@@ -35,6 +35,148 @@ pnpm add @ahoo-wang/fetcher-eventstream
|
|
|
35
35
|
yarn add @ahoo-wang/fetcher-eventstream
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
### 集成测试示例:带事件流的 LLM 客户端
|
|
39
|
+
|
|
40
|
+
以下示例展示了如何创建带事件流支持的 LLM 客户端,类似于 Fetcher
|
|
41
|
+
项目中的集成测试。您可以在 [integration-test/src/eventstream/llmClient.ts](../../integration-test/src/eventstream/llmClient.ts)
|
|
42
|
+
中找到完整实现。
|
|
43
|
+
|
|
44
|
+
这个示例演示了如何使用 Fetcher 的流式传输功能与流行的 LLM API(如 OpenAI 的 GPT 模型)进行交互。
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import {
|
|
48
|
+
BaseURLCapable,
|
|
49
|
+
ContentTypeValues,
|
|
50
|
+
FetchExchange,
|
|
51
|
+
NamedFetcher,
|
|
52
|
+
REQUEST_BODY_INTERCEPTOR_ORDER,
|
|
53
|
+
RequestInterceptor,
|
|
54
|
+
} from '@ahoo-wang/fetcher';
|
|
55
|
+
import {
|
|
56
|
+
api,
|
|
57
|
+
autoGeneratedError,
|
|
58
|
+
body,
|
|
59
|
+
post,
|
|
60
|
+
ResultExtractors,
|
|
61
|
+
} from '@ahoo-wang/fetcher-decorator';
|
|
62
|
+
import {
|
|
63
|
+
EventStreamInterceptor,
|
|
64
|
+
JsonServerSentEventStream,
|
|
65
|
+
} from '@ahoo-wang/fetcher-eventstream';
|
|
66
|
+
import { ChatRequest, ChatResponse } from './types';
|
|
67
|
+
|
|
68
|
+
export const llmFetcherName = 'llm';
|
|
69
|
+
|
|
70
|
+
export interface LlmOptions extends BaseURLCapable {
|
|
71
|
+
apiKey: string;
|
|
72
|
+
model?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class LlmRequestInterceptor implements RequestInterceptor {
|
|
76
|
+
readonly name: string = 'LlmRequestInterceptor';
|
|
77
|
+
readonly order: number = REQUEST_BODY_INTERCEPTOR_ORDER - 1;
|
|
78
|
+
|
|
79
|
+
constructor(private llmOptions: LlmOptions) {
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
intercept(exchange: FetchExchange): void {
|
|
83
|
+
const chatRequest = exchange.request.body as ChatRequest;
|
|
84
|
+
if (!chatRequest.model) {
|
|
85
|
+
chatRequest.model = this.llmOptions.model;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createLlmFetcher(options: LlmOptions): NamedFetcher {
|
|
91
|
+
const llmFetcher = new NamedFetcher(llmFetcherName, {
|
|
92
|
+
baseURL: options.baseURL,
|
|
93
|
+
headers: {
|
|
94
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
95
|
+
'Content-Type': ContentTypeValues.APPLICATION_JSON,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
llmFetcher.interceptors.request.use(new LlmRequestInterceptor(options));
|
|
99
|
+
llmFetcher.interceptors.response.use(new EventStreamInterceptor());
|
|
100
|
+
return llmFetcher;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@api('/chat', {
|
|
104
|
+
fetcher: llmFetcherName,
|
|
105
|
+
resultExtractor: ResultExtractors.JsonEventStream,
|
|
106
|
+
})
|
|
107
|
+
export class LlmClient {
|
|
108
|
+
@post('/completions')
|
|
109
|
+
streamChat(
|
|
110
|
+
@body() _body: ChatRequest,
|
|
111
|
+
): Promise<JsonServerSentEventStream<ChatResponse>> {
|
|
112
|
+
throw autoGeneratedError();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@post('/completions', { resultExtractor: ResultExtractors.Json })
|
|
116
|
+
chat(@body() _body: ChatRequest): Promise<ChatResponse> {
|
|
117
|
+
throw autoGeneratedError();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### 使用 streamChat 进行实时响应
|
|
123
|
+
|
|
124
|
+
以下是使用 `streamChat` 方法从 LLM API 获取实时响应的示例:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { createLlmFetcher, LlmClient } from './llmClient';
|
|
128
|
+
|
|
129
|
+
// 使用您的 API 配置初始化 LLM 客户端
|
|
130
|
+
const llmFetcher = createLlmFetcher({
|
|
131
|
+
baseURL: 'https://api.openai.com/v1', // OpenAI 示例
|
|
132
|
+
apiKey: process.env.OPENAI_API_KEY || 'your-api-key',
|
|
133
|
+
model: 'gpt-3.5-turbo', // 默认模型
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 创建客户端实例
|
|
137
|
+
const llmClient = new LlmClient();
|
|
138
|
+
|
|
139
|
+
// 示例:实时流式传输聊天完成响应
|
|
140
|
+
async function streamChatExample() {
|
|
141
|
+
try {
|
|
142
|
+
// 流式传输响应,逐个令牌接收
|
|
143
|
+
const stream = await llmClient.streamChat({
|
|
144
|
+
messages: [
|
|
145
|
+
{ role: 'system', content: 'You are a helpful assistant.' },
|
|
146
|
+
{ role: 'user', content: 'Explain quantum computing in simple terms.' },
|
|
147
|
+
],
|
|
148
|
+
model: 'gpt-3.5-turbo', // 如需要可覆盖默认模型
|
|
149
|
+
stream: true, // 启用流式传输
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// 处理流式响应
|
|
153
|
+
let fullResponse = '';
|
|
154
|
+
for await (const event of stream) {
|
|
155
|
+
// 每个事件包含部分响应
|
|
156
|
+
if (event.data) {
|
|
157
|
+
const chunk = event.data;
|
|
158
|
+
const content = chunk.choices[0]?.delta?.content || '';
|
|
159
|
+
fullResponse += content;
|
|
160
|
+
console.log('新令牌:', content);
|
|
161
|
+
|
|
162
|
+
// 在令牌到达时实时更新 UI
|
|
163
|
+
updateUI(content);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log('完整响应:', fullResponse);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('流式聊天错误:', error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 辅助函数模拟 UI 更新
|
|
174
|
+
function updateUI(content: string) {
|
|
175
|
+
// 在实际应用中,这将更新您的 UI
|
|
176
|
+
process.stdout.write(content);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
38
180
|
### 带拦截器的基本用法
|
|
39
181
|
|
|
40
182
|
```typescript
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eventStreamInterceptor.d.ts","sourceRoot":"","sources":["../src/eventStreamInterceptor.ts"],"names":[],"mappings":"AAcA,OAAO,EAGL,aAAa,EACb,mBAAmB,EACpB,MAAM,oBAAoB,CAAC;AAG5B;;GAEG;AACH,eAAO,MAAM,6BAA6B,2BAA2B,CAAC;AAEtE;;;GAGG;AACH,eAAO,MAAM,8BAA8B,QAAiC,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IAChE,QAAQ,CAAC,IAAI,4BAAiC;IAC9C,QAAQ,CAAC,KAAK,SAAkC;IAEhD;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa;
|
|
1
|
+
{"version":3,"file":"eventStreamInterceptor.d.ts","sourceRoot":"","sources":["../src/eventStreamInterceptor.ts"],"names":[],"mappings":"AAcA,OAAO,EAGL,aAAa,EACb,mBAAmB,EACpB,MAAM,oBAAoB,CAAC;AAG5B;;GAEG;AACH,eAAO,MAAM,6BAA6B,2BAA2B,CAAC;AAEtE;;;GAGG;AACH,eAAO,MAAM,8BAA8B,QAAiC,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IAChE,QAAQ,CAAC,IAAI,4BAAiC;IAC9C,QAAQ,CAAC,KAAK,SAAkC;IAEhD;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa;CAalC"}
|
package/dist/index.es.js
CHANGED
package/dist/index.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","sources":["../src/textLineTransformStream.ts","../src/serverSentEventTransformStream.ts","../src/eventStreamConverter.ts","../src/jsonServerSentEventTransformStream.ts","../src/eventStreamInterceptor.ts"],"sourcesContent":["/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Transformer that splits text into lines.\n *\n * This transformer accumulates chunks of text and splits them by newline characters,\n * emitting each line as a separate chunk while preserving the remaining buffer\n * for the next chunk.\n */\nexport class TextLineTransformer implements Transformer<string, string> {\n private buffer = '';\n\n /**\n * Transform input string chunk by splitting it into lines.\n *\n * @param chunk Input string chunk\n * @param controller Controller for controlling the transform stream\n */\n transform(\n chunk: string,\n controller: TransformStreamDefaultController<string>,\n ) {\n try {\n this.buffer += chunk;\n const lines = this.buffer.split('\\n');\n this.buffer = lines.pop() || '';\n\n for (const line of lines) {\n controller.enqueue(line);\n }\n } catch (error) {\n controller.error(error);\n }\n }\n\n /**\n * Flush remaining buffer when the stream ends.\n *\n * @param controller Controller for controlling the transform stream\n */\n flush(controller: TransformStreamDefaultController<string>) {\n try {\n // Only send when buffer is not empty, avoid sending meaningless empty lines\n if (this.buffer) {\n controller.enqueue(this.buffer);\n }\n } catch (error) {\n controller.error(error);\n }\n }\n}\n\n/**\n * A TransformStream that splits text into lines.\n */\nexport class TextLineTransformStream extends TransformStream<string, string> {\n constructor() {\n super(new TextLineTransformer());\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Represents a message sent in an event stream.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format}\n */\nexport interface ServerSentEvent {\n /** The event ID to set the EventSource object's last event ID value. */\n id?: string;\n /** A string identifying the type of event described. */\n event: string;\n /** The event data */\n data: string;\n /** The reconnection interval (in milliseconds) to wait before retrying the connection */\n retry?: number;\n}\n\nexport enum ServerSentEventField {\n ID = 'id',\n RETRY = 'retry',\n EVENT = 'event',\n DATA = 'data',\n}\n\n/**\n * Process field value\n * @param field Field name\n * @param value Field value\n * @param currentEvent Current event state\n */\nfunction processFieldInternal(\n field: string,\n value: string,\n currentEvent: EventState,\n) {\n switch (field) {\n case ServerSentEventField.EVENT:\n currentEvent.event = value;\n break;\n case ServerSentEventField.DATA:\n currentEvent.data.push(value);\n break;\n case ServerSentEventField.ID:\n currentEvent.id = value;\n break;\n case ServerSentEventField.RETRY: {\n const retryValue = parseInt(value, 10);\n if (!isNaN(retryValue)) {\n currentEvent.retry = retryValue;\n }\n break;\n }\n default:\n // Ignore unknown fields\n break;\n }\n}\n\ninterface EventState {\n event?: string;\n id?: string;\n retry?: number;\n data: string[];\n}\n\nconst DEFAULT_EVENT_TYPE = 'message';\n\n/**\n * Transformer responsible for converting a string stream into a ServerSentEvent object stream.\n *\n * Implements the Transformer interface for processing data transformation in TransformStream.\n */\nexport class ServerSentEventTransformer\n implements Transformer<string, ServerSentEvent>\n{\n // Initialize currentEvent with default values in a closure\n private currentEvent: EventState = {\n event: DEFAULT_EVENT_TYPE,\n id: undefined,\n retry: undefined,\n data: [],\n };\n\n /**\n * Transform input string chunk into ServerSentEvent object.\n *\n * @param chunk Input string chunk\n * @param controller Controller for controlling the transform stream\n */\n transform(\n chunk: string,\n controller: TransformStreamDefaultController<ServerSentEvent>,\n ) {\n const currentEvent = this.currentEvent;\n try {\n // Skip empty lines (event separator)\n if (chunk.trim() === '') {\n // If there is accumulated event data, send event\n if (currentEvent.data.length > 0) {\n controller.enqueue({\n event: currentEvent.event || DEFAULT_EVENT_TYPE,\n data: currentEvent.data.join('\\n'),\n id: currentEvent.id || '',\n retry: currentEvent.retry,\n } as ServerSentEvent);\n\n // Reset current event (preserve id and retry for subsequent events)\n currentEvent.event = DEFAULT_EVENT_TYPE;\n // Preserve id and retry for subsequent events (no need to reassign to themselves)\n currentEvent.data = [];\n }\n return;\n }\n\n // Ignore comment lines (starting with colon)\n if (chunk.startsWith(':')) {\n return;\n }\n\n // Parse fields\n const colonIndex = chunk.indexOf(':');\n let field: string;\n let value: string;\n\n if (colonIndex === -1) {\n // No colon, entire line as field name, value is empty\n field = chunk.toLowerCase();\n value = '';\n } else {\n // Extract field name and value\n field = chunk.substring(0, colonIndex).toLowerCase();\n value = chunk.substring(colonIndex + 1);\n\n // If value starts with space, remove leading space\n if (value.startsWith(' ')) {\n value = value.substring(1);\n }\n }\n\n // Remove trailing newlines from field and value\n field = field.trim();\n value = value.trim();\n\n processFieldInternal(field, value, currentEvent);\n } catch (error) {\n controller.error(\n error instanceof Error ? error : new Error(String(error)),\n );\n // Reset state\n currentEvent.event = DEFAULT_EVENT_TYPE;\n currentEvent.id = undefined;\n currentEvent.retry = undefined;\n currentEvent.data = [];\n }\n }\n\n /**\n * Called when the stream ends, used to process remaining data.\n *\n * @param controller Controller for controlling the transform stream\n */\n flush(controller: TransformStreamDefaultController<ServerSentEvent>) {\n const currentEvent = this.currentEvent;\n try {\n // Send the last event (if any)\n if (currentEvent.data.length > 0) {\n controller.enqueue({\n event: currentEvent.event || DEFAULT_EVENT_TYPE,\n data: currentEvent.data.join('\\n'),\n id: currentEvent.id || '',\n retry: currentEvent.retry,\n } as ServerSentEvent);\n }\n } catch (error) {\n controller.error(\n error instanceof Error ? error : new Error(String(error)),\n );\n } finally {\n // Reset state\n currentEvent.event = DEFAULT_EVENT_TYPE;\n currentEvent.id = undefined;\n currentEvent.retry = undefined;\n currentEvent.data = [];\n }\n }\n}\n\n/**\n * A TransformStream that converts a stream of strings into a stream of ServerSentEvent objects.\n */\nexport class ServerSentEventTransformStream extends TransformStream<\n string,\n ServerSentEvent\n> {\n constructor() {\n super(new ServerSentEventTransformer());\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { TextLineTransformStream } from './textLineTransformStream';\nimport {\n ServerSentEvent,\n ServerSentEventTransformStream,\n} from './serverSentEventTransformStream';\n\n/**\n * A ReadableStream of ServerSentEvent objects.\n */\nexport type ServerSentEventStream = ReadableStream<ServerSentEvent>;\n\n/**\n * Converts a Response object to a ServerSentEventStream.\n *\n * Processes the response body through a series of transform streams:\n * 1. TextDecoderStream: Decode Uint8Array data to UTF-8 strings\n * 2. TextLineStream: Split text by lines\n * 3. ServerSentEventStream: Parse line data into server-sent events\n *\n * @param response - The Response object to convert\n * @returns A ReadableStream of ServerSentEvent objects\n * @throws Error if the response body is null\n */\nexport function toServerSentEventStream(\n response: Response,\n): ServerSentEventStream {\n if (!response.body) {\n throw new Error('Response body is null');\n }\n\n return response.body\n .pipeThrough(new TextDecoderStream('utf-8'))\n .pipeThrough(new TextLineTransformStream())\n .pipeThrough(new ServerSentEventTransformStream());\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ServerSentEvent } from './serverSentEventTransformStream';\nimport { ServerSentEventStream } from './eventStreamConverter';\n\nexport interface JsonServerSentEvent<DATA> extends Omit<ServerSentEvent, 'data'> {\n data: DATA;\n}\n\nexport class JsonServerSentEventTransform<DATA> implements Transformer<ServerSentEvent, JsonServerSentEvent<DATA>> {\n transform(\n chunk: ServerSentEvent,\n controller: TransformStreamDefaultController<JsonServerSentEvent<DATA>>,\n ) {\n const json = JSON.parse(chunk.data) as DATA;\n controller.enqueue({\n data: json,\n event: chunk.event,\n id: chunk.id,\n retry: chunk.retry,\n });\n }\n}\n\nexport class JsonServerSentEventTransformStream<DATA> extends TransformStream<ServerSentEvent, JsonServerSentEvent<DATA>> {\n constructor() {\n super(new JsonServerSentEventTransform());\n }\n}\n\nexport type JsonServerSentEventStream<DATA> = ReadableStream<JsonServerSentEvent<DATA>>;\n\nexport function toJsonServerSentEventStream<DATA>(\n serverSentEventStream: ServerSentEventStream,\n): JsonServerSentEventStream<DATA> {\n return serverSentEventStream.pipeThrough(new JsonServerSentEventTransformStream<DATA>());\n}","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { toServerSentEventStream } from './eventStreamConverter';\nimport {\n ContentTypeHeader,\n ContentTypeValues,\n FetchExchange,\n ResponseInterceptor,\n} from '@ahoo-wang/fetcher';\nimport { toJsonServerSentEventStream } from './jsonServerSentEventTransformStream';\n\n/**\n * The name of the EventStreamInterceptor.\n */\nexport const EVENT_STREAM_INTERCEPTOR_NAME = 'EventStreamInterceptor';\n\n/**\n * The order of the EventStreamInterceptor.\n * Set to Number.MAX_SAFE_INTEGER - 1000 to ensure it runs latest among response interceptors.\n */\nexport const EVENT_STREAM_INTERCEPTOR_ORDER = Number.MAX_SAFE_INTEGER - 1000;\n\n/**\n * Interceptor that enhances Response objects with event stream capabilities.\n *\n * This interceptor detects responses with `text/event-stream` content type and adds\n * an `eventStream()` method to the Response object, which returns a readable stream\n * of Server-Sent Events that can be consumed using `for await` syntax.\n *\n * @remarks\n * This interceptor runs at the very end of the response interceptor chain to ensure\n * it runs after all standard response processing is complete, as it adds\n * specialized functionality to the response object. The order is set to\n * EVENT_STREAM_INTERCEPTOR_ORDER to ensure it executes latest among response interceptors,\n * allowing for other response interceptors to run before it if needed. This positioning\n * ensures that all response processing is completed before specialized event stream\n * functionality is added to the response object.\n *\n * @example\n * ```typescript\n * // Using the eventStream method\n * const response = await fetcher.get('/events');\n * if (response.headers.get('content-type')?.includes('text/event-stream')) {\n * const eventStream = response.eventStream();\n * for await (const event of eventStream) {\n * console.log('Received event:', event);\n * }\n * }\n * ```\n */\nexport class EventStreamInterceptor implements ResponseInterceptor {\n readonly name = EVENT_STREAM_INTERCEPTOR_NAME;\n readonly order = EVENT_STREAM_INTERCEPTOR_ORDER;\n\n /**\n * Intercepts responses to add event stream capabilities.\n *\n * This method runs at the very end of the response interceptor chain to ensure\n * it runs after all standard response processing is complete. It detects responses\n * with `text/event-stream` content type and adds an `eventStream()` method to\n * the Response object, which returns a readable stream of Server-Sent Events.\n *\n * @param exchange - The exchange containing the response to enhance\n *\n * @remarks\n * This method executes latest among response interceptors to ensure all response\n * processing is completed before specialized event stream functionality is added.\n * It only enhances responses with `text/event-stream` content type, leaving other\n * responses unchanged. The positioning at the end of the response chain ensures\n * that all response transformations and validations are completed before event\n * stream capabilities are added to the response object.\n */\n intercept(exchange: FetchExchange) {\n // Check if the response is an event stream\n const response = exchange.response;\n if (!response) {\n return;\n }\n const contentType = response.headers.get(ContentTypeHeader);\n if (contentType?.includes(ContentTypeValues.TEXT_EVENT_STREAM)) {\n response.eventStream = () => toServerSentEventStream(response);\n response.jsonEventStream = () => toJsonServerSentEventStream(toServerSentEventStream(response));\n }\n }\n}\n"],"names":["TextLineTransformer","chunk","controller","lines","line","error","TextLineTransformStream","ServerSentEventField","processFieldInternal","field","value","currentEvent","retryValue","DEFAULT_EVENT_TYPE","ServerSentEventTransformer","colonIndex","ServerSentEventTransformStream","toServerSentEventStream","response","JsonServerSentEventTransform","json","JsonServerSentEventTransformStream","toJsonServerSentEventStream","serverSentEventStream","EVENT_STREAM_INTERCEPTOR_NAME","EVENT_STREAM_INTERCEPTOR_ORDER","EventStreamInterceptor","exchange","ContentTypeHeader","ContentTypeValues"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.es.js","sources":["../src/textLineTransformStream.ts","../src/serverSentEventTransformStream.ts","../src/eventStreamConverter.ts","../src/jsonServerSentEventTransformStream.ts","../src/eventStreamInterceptor.ts"],"sourcesContent":["/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Transformer that splits text into lines.\n *\n * This transformer accumulates chunks of text and splits them by newline characters,\n * emitting each line as a separate chunk while preserving the remaining buffer\n * for the next chunk.\n */\nexport class TextLineTransformer implements Transformer<string, string> {\n private buffer = '';\n\n /**\n * Transform input string chunk by splitting it into lines.\n *\n * @param chunk Input string chunk\n * @param controller Controller for controlling the transform stream\n */\n transform(\n chunk: string,\n controller: TransformStreamDefaultController<string>,\n ) {\n try {\n this.buffer += chunk;\n const lines = this.buffer.split('\\n');\n this.buffer = lines.pop() || '';\n\n for (const line of lines) {\n controller.enqueue(line);\n }\n } catch (error) {\n controller.error(error);\n }\n }\n\n /**\n * Flush remaining buffer when the stream ends.\n *\n * @param controller Controller for controlling the transform stream\n */\n flush(controller: TransformStreamDefaultController<string>) {\n try {\n // Only send when buffer is not empty, avoid sending meaningless empty lines\n if (this.buffer) {\n controller.enqueue(this.buffer);\n }\n } catch (error) {\n controller.error(error);\n }\n }\n}\n\n/**\n * A TransformStream that splits text into lines.\n */\nexport class TextLineTransformStream extends TransformStream<string, string> {\n constructor() {\n super(new TextLineTransformer());\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Represents a message sent in an event stream.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format}\n */\nexport interface ServerSentEvent {\n /** The event ID to set the EventSource object's last event ID value. */\n id?: string;\n /** A string identifying the type of event described. */\n event: string;\n /** The event data */\n data: string;\n /** The reconnection interval (in milliseconds) to wait before retrying the connection */\n retry?: number;\n}\n\nexport enum ServerSentEventField {\n ID = 'id',\n RETRY = 'retry',\n EVENT = 'event',\n DATA = 'data',\n}\n\n/**\n * Process field value\n * @param field Field name\n * @param value Field value\n * @param currentEvent Current event state\n */\nfunction processFieldInternal(\n field: string,\n value: string,\n currentEvent: EventState,\n) {\n switch (field) {\n case ServerSentEventField.EVENT:\n currentEvent.event = value;\n break;\n case ServerSentEventField.DATA:\n currentEvent.data.push(value);\n break;\n case ServerSentEventField.ID:\n currentEvent.id = value;\n break;\n case ServerSentEventField.RETRY: {\n const retryValue = parseInt(value, 10);\n if (!isNaN(retryValue)) {\n currentEvent.retry = retryValue;\n }\n break;\n }\n default:\n // Ignore unknown fields\n break;\n }\n}\n\ninterface EventState {\n event?: string;\n id?: string;\n retry?: number;\n data: string[];\n}\n\nconst DEFAULT_EVENT_TYPE = 'message';\n\n/**\n * Transformer responsible for converting a string stream into a ServerSentEvent object stream.\n *\n * Implements the Transformer interface for processing data transformation in TransformStream.\n */\nexport class ServerSentEventTransformer\n implements Transformer<string, ServerSentEvent>\n{\n // Initialize currentEvent with default values in a closure\n private currentEvent: EventState = {\n event: DEFAULT_EVENT_TYPE,\n id: undefined,\n retry: undefined,\n data: [],\n };\n\n /**\n * Transform input string chunk into ServerSentEvent object.\n *\n * @param chunk Input string chunk\n * @param controller Controller for controlling the transform stream\n */\n transform(\n chunk: string,\n controller: TransformStreamDefaultController<ServerSentEvent>,\n ) {\n const currentEvent = this.currentEvent;\n try {\n // Skip empty lines (event separator)\n if (chunk.trim() === '') {\n // If there is accumulated event data, send event\n if (currentEvent.data.length > 0) {\n controller.enqueue({\n event: currentEvent.event || DEFAULT_EVENT_TYPE,\n data: currentEvent.data.join('\\n'),\n id: currentEvent.id || '',\n retry: currentEvent.retry,\n } as ServerSentEvent);\n\n // Reset current event (preserve id and retry for subsequent events)\n currentEvent.event = DEFAULT_EVENT_TYPE;\n // Preserve id and retry for subsequent events (no need to reassign to themselves)\n currentEvent.data = [];\n }\n return;\n }\n\n // Ignore comment lines (starting with colon)\n if (chunk.startsWith(':')) {\n return;\n }\n\n // Parse fields\n const colonIndex = chunk.indexOf(':');\n let field: string;\n let value: string;\n\n if (colonIndex === -1) {\n // No colon, entire line as field name, value is empty\n field = chunk.toLowerCase();\n value = '';\n } else {\n // Extract field name and value\n field = chunk.substring(0, colonIndex).toLowerCase();\n value = chunk.substring(colonIndex + 1);\n\n // If value starts with space, remove leading space\n if (value.startsWith(' ')) {\n value = value.substring(1);\n }\n }\n\n // Remove trailing newlines from field and value\n field = field.trim();\n value = value.trim();\n\n processFieldInternal(field, value, currentEvent);\n } catch (error) {\n controller.error(\n error instanceof Error ? error : new Error(String(error)),\n );\n // Reset state\n currentEvent.event = DEFAULT_EVENT_TYPE;\n currentEvent.id = undefined;\n currentEvent.retry = undefined;\n currentEvent.data = [];\n }\n }\n\n /**\n * Called when the stream ends, used to process remaining data.\n *\n * @param controller Controller for controlling the transform stream\n */\n flush(controller: TransformStreamDefaultController<ServerSentEvent>) {\n const currentEvent = this.currentEvent;\n try {\n // Send the last event (if any)\n if (currentEvent.data.length > 0) {\n controller.enqueue({\n event: currentEvent.event || DEFAULT_EVENT_TYPE,\n data: currentEvent.data.join('\\n'),\n id: currentEvent.id || '',\n retry: currentEvent.retry,\n } as ServerSentEvent);\n }\n } catch (error) {\n controller.error(\n error instanceof Error ? error : new Error(String(error)),\n );\n } finally {\n // Reset state\n currentEvent.event = DEFAULT_EVENT_TYPE;\n currentEvent.id = undefined;\n currentEvent.retry = undefined;\n currentEvent.data = [];\n }\n }\n}\n\n/**\n * A TransformStream that converts a stream of strings into a stream of ServerSentEvent objects.\n */\nexport class ServerSentEventTransformStream extends TransformStream<\n string,\n ServerSentEvent\n> {\n constructor() {\n super(new ServerSentEventTransformer());\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { TextLineTransformStream } from './textLineTransformStream';\nimport {\n ServerSentEvent,\n ServerSentEventTransformStream,\n} from './serverSentEventTransformStream';\n\n/**\n * A ReadableStream of ServerSentEvent objects.\n */\nexport type ServerSentEventStream = ReadableStream<ServerSentEvent>;\n\n/**\n * Converts a Response object to a ServerSentEventStream.\n *\n * Processes the response body through a series of transform streams:\n * 1. TextDecoderStream: Decode Uint8Array data to UTF-8 strings\n * 2. TextLineStream: Split text by lines\n * 3. ServerSentEventStream: Parse line data into server-sent events\n *\n * @param response - The Response object to convert\n * @returns A ReadableStream of ServerSentEvent objects\n * @throws Error if the response body is null\n */\nexport function toServerSentEventStream(\n response: Response,\n): ServerSentEventStream {\n if (!response.body) {\n throw new Error('Response body is null');\n }\n\n return response.body\n .pipeThrough(new TextDecoderStream('utf-8'))\n .pipeThrough(new TextLineTransformStream())\n .pipeThrough(new ServerSentEventTransformStream());\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ServerSentEvent } from './serverSentEventTransformStream';\nimport { ServerSentEventStream } from './eventStreamConverter';\n\nexport interface JsonServerSentEvent<DATA>\n extends Omit<ServerSentEvent, 'data'> {\n data: DATA;\n}\n\nexport class JsonServerSentEventTransform<DATA>\n implements Transformer<ServerSentEvent, JsonServerSentEvent<DATA>> {\n transform(\n chunk: ServerSentEvent,\n controller: TransformStreamDefaultController<JsonServerSentEvent<DATA>>,\n ) {\n const json = JSON.parse(chunk.data) as DATA;\n controller.enqueue({\n data: json,\n event: chunk.event,\n id: chunk.id,\n retry: chunk.retry,\n });\n }\n}\n\nexport class JsonServerSentEventTransformStream<DATA> extends TransformStream<\n ServerSentEvent,\n JsonServerSentEvent<DATA>\n> {\n constructor() {\n super(new JsonServerSentEventTransform());\n }\n}\n\nexport type JsonServerSentEventStream<DATA> = ReadableStream<\n JsonServerSentEvent<DATA>\n>;\n\nexport function toJsonServerSentEventStream<DATA>(\n serverSentEventStream: ServerSentEventStream,\n): JsonServerSentEventStream<DATA> {\n return serverSentEventStream.pipeThrough(\n new JsonServerSentEventTransformStream<DATA>(),\n );\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { toServerSentEventStream } from './eventStreamConverter';\nimport {\n ContentTypeHeader,\n ContentTypeValues,\n FetchExchange,\n ResponseInterceptor,\n} from '@ahoo-wang/fetcher';\nimport { toJsonServerSentEventStream } from './jsonServerSentEventTransformStream';\n\n/**\n * The name of the EventStreamInterceptor.\n */\nexport const EVENT_STREAM_INTERCEPTOR_NAME = 'EventStreamInterceptor';\n\n/**\n * The order of the EventStreamInterceptor.\n * Set to Number.MAX_SAFE_INTEGER - 1000 to ensure it runs latest among response interceptors.\n */\nexport const EVENT_STREAM_INTERCEPTOR_ORDER = Number.MAX_SAFE_INTEGER - 1000;\n\n/**\n * Interceptor that enhances Response objects with event stream capabilities.\n *\n * This interceptor detects responses with `text/event-stream` content type and adds\n * an `eventStream()` method to the Response object, which returns a readable stream\n * of Server-Sent Events that can be consumed using `for await` syntax.\n *\n * @remarks\n * This interceptor runs at the very end of the response interceptor chain to ensure\n * it runs after all standard response processing is complete, as it adds\n * specialized functionality to the response object. The order is set to\n * EVENT_STREAM_INTERCEPTOR_ORDER to ensure it executes latest among response interceptors,\n * allowing for other response interceptors to run before it if needed. This positioning\n * ensures that all response processing is completed before specialized event stream\n * functionality is added to the response object.\n *\n * @example\n * ```typescript\n * // Using the eventStream method\n * const response = await fetcher.get('/events');\n * if (response.headers.get('content-type')?.includes('text/event-stream')) {\n * const eventStream = response.eventStream();\n * for await (const event of eventStream) {\n * console.log('Received event:', event);\n * }\n * }\n * ```\n */\nexport class EventStreamInterceptor implements ResponseInterceptor {\n readonly name = EVENT_STREAM_INTERCEPTOR_NAME;\n readonly order = EVENT_STREAM_INTERCEPTOR_ORDER;\n\n /**\n * Intercepts responses to add event stream capabilities.\n *\n * This method runs at the very end of the response interceptor chain to ensure\n * it runs after all standard response processing is complete. It detects responses\n * with `text/event-stream` content type and adds an `eventStream()` method to\n * the Response object, which returns a readable stream of Server-Sent Events.\n *\n * @param exchange - The exchange containing the response to enhance\n *\n * @remarks\n * This method executes latest among response interceptors to ensure all response\n * processing is completed before specialized event stream functionality is added.\n * It only enhances responses with `text/event-stream` content type, leaving other\n * responses unchanged. The positioning at the end of the response chain ensures\n * that all response transformations and validations are completed before event\n * stream capabilities are added to the response object.\n */\n intercept(exchange: FetchExchange) {\n // Check if the response is an event stream\n const response = exchange.response;\n if (!response) {\n return;\n }\n const contentType = response.headers.get(ContentTypeHeader);\n if (contentType?.includes(ContentTypeValues.TEXT_EVENT_STREAM)) {\n response.eventStream = () => toServerSentEventStream(response);\n response.jsonEventStream = () =>\n toJsonServerSentEventStream(toServerSentEventStream(response));\n }\n }\n}\n"],"names":["TextLineTransformer","chunk","controller","lines","line","error","TextLineTransformStream","ServerSentEventField","processFieldInternal","field","value","currentEvent","retryValue","DEFAULT_EVENT_TYPE","ServerSentEventTransformer","colonIndex","ServerSentEventTransformStream","toServerSentEventStream","response","JsonServerSentEventTransform","json","JsonServerSentEventTransformStream","toJsonServerSentEventStream","serverSentEventStream","EVENT_STREAM_INTERCEPTOR_NAME","EVENT_STREAM_INTERCEPTOR_ORDER","EventStreamInterceptor","exchange","ContentTypeHeader","ContentTypeValues"],"mappings":"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"}
|
package/dist/index.umd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.umd.js","sources":["../src/textLineTransformStream.ts","../src/serverSentEventTransformStream.ts","../src/eventStreamConverter.ts","../src/jsonServerSentEventTransformStream.ts","../src/eventStreamInterceptor.ts"],"sourcesContent":["/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Transformer that splits text into lines.\n *\n * This transformer accumulates chunks of text and splits them by newline characters,\n * emitting each line as a separate chunk while preserving the remaining buffer\n * for the next chunk.\n */\nexport class TextLineTransformer implements Transformer<string, string> {\n private buffer = '';\n\n /**\n * Transform input string chunk by splitting it into lines.\n *\n * @param chunk Input string chunk\n * @param controller Controller for controlling the transform stream\n */\n transform(\n chunk: string,\n controller: TransformStreamDefaultController<string>,\n ) {\n try {\n this.buffer += chunk;\n const lines = this.buffer.split('\\n');\n this.buffer = lines.pop() || '';\n\n for (const line of lines) {\n controller.enqueue(line);\n }\n } catch (error) {\n controller.error(error);\n }\n }\n\n /**\n * Flush remaining buffer when the stream ends.\n *\n * @param controller Controller for controlling the transform stream\n */\n flush(controller: TransformStreamDefaultController<string>) {\n try {\n // Only send when buffer is not empty, avoid sending meaningless empty lines\n if (this.buffer) {\n controller.enqueue(this.buffer);\n }\n } catch (error) {\n controller.error(error);\n }\n }\n}\n\n/**\n * A TransformStream that splits text into lines.\n */\nexport class TextLineTransformStream extends TransformStream<string, string> {\n constructor() {\n super(new TextLineTransformer());\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Represents a message sent in an event stream.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format}\n */\nexport interface ServerSentEvent {\n /** The event ID to set the EventSource object's last event ID value. */\n id?: string;\n /** A string identifying the type of event described. */\n event: string;\n /** The event data */\n data: string;\n /** The reconnection interval (in milliseconds) to wait before retrying the connection */\n retry?: number;\n}\n\nexport enum ServerSentEventField {\n ID = 'id',\n RETRY = 'retry',\n EVENT = 'event',\n DATA = 'data',\n}\n\n/**\n * Process field value\n * @param field Field name\n * @param value Field value\n * @param currentEvent Current event state\n */\nfunction processFieldInternal(\n field: string,\n value: string,\n currentEvent: EventState,\n) {\n switch (field) {\n case ServerSentEventField.EVENT:\n currentEvent.event = value;\n break;\n case ServerSentEventField.DATA:\n currentEvent.data.push(value);\n break;\n case ServerSentEventField.ID:\n currentEvent.id = value;\n break;\n case ServerSentEventField.RETRY: {\n const retryValue = parseInt(value, 10);\n if (!isNaN(retryValue)) {\n currentEvent.retry = retryValue;\n }\n break;\n }\n default:\n // Ignore unknown fields\n break;\n }\n}\n\ninterface EventState {\n event?: string;\n id?: string;\n retry?: number;\n data: string[];\n}\n\nconst DEFAULT_EVENT_TYPE = 'message';\n\n/**\n * Transformer responsible for converting a string stream into a ServerSentEvent object stream.\n *\n * Implements the Transformer interface for processing data transformation in TransformStream.\n */\nexport class ServerSentEventTransformer\n implements Transformer<string, ServerSentEvent>\n{\n // Initialize currentEvent with default values in a closure\n private currentEvent: EventState = {\n event: DEFAULT_EVENT_TYPE,\n id: undefined,\n retry: undefined,\n data: [],\n };\n\n /**\n * Transform input string chunk into ServerSentEvent object.\n *\n * @param chunk Input string chunk\n * @param controller Controller for controlling the transform stream\n */\n transform(\n chunk: string,\n controller: TransformStreamDefaultController<ServerSentEvent>,\n ) {\n const currentEvent = this.currentEvent;\n try {\n // Skip empty lines (event separator)\n if (chunk.trim() === '') {\n // If there is accumulated event data, send event\n if (currentEvent.data.length > 0) {\n controller.enqueue({\n event: currentEvent.event || DEFAULT_EVENT_TYPE,\n data: currentEvent.data.join('\\n'),\n id: currentEvent.id || '',\n retry: currentEvent.retry,\n } as ServerSentEvent);\n\n // Reset current event (preserve id and retry for subsequent events)\n currentEvent.event = DEFAULT_EVENT_TYPE;\n // Preserve id and retry for subsequent events (no need to reassign to themselves)\n currentEvent.data = [];\n }\n return;\n }\n\n // Ignore comment lines (starting with colon)\n if (chunk.startsWith(':')) {\n return;\n }\n\n // Parse fields\n const colonIndex = chunk.indexOf(':');\n let field: string;\n let value: string;\n\n if (colonIndex === -1) {\n // No colon, entire line as field name, value is empty\n field = chunk.toLowerCase();\n value = '';\n } else {\n // Extract field name and value\n field = chunk.substring(0, colonIndex).toLowerCase();\n value = chunk.substring(colonIndex + 1);\n\n // If value starts with space, remove leading space\n if (value.startsWith(' ')) {\n value = value.substring(1);\n }\n }\n\n // Remove trailing newlines from field and value\n field = field.trim();\n value = value.trim();\n\n processFieldInternal(field, value, currentEvent);\n } catch (error) {\n controller.error(\n error instanceof Error ? error : new Error(String(error)),\n );\n // Reset state\n currentEvent.event = DEFAULT_EVENT_TYPE;\n currentEvent.id = undefined;\n currentEvent.retry = undefined;\n currentEvent.data = [];\n }\n }\n\n /**\n * Called when the stream ends, used to process remaining data.\n *\n * @param controller Controller for controlling the transform stream\n */\n flush(controller: TransformStreamDefaultController<ServerSentEvent>) {\n const currentEvent = this.currentEvent;\n try {\n // Send the last event (if any)\n if (currentEvent.data.length > 0) {\n controller.enqueue({\n event: currentEvent.event || DEFAULT_EVENT_TYPE,\n data: currentEvent.data.join('\\n'),\n id: currentEvent.id || '',\n retry: currentEvent.retry,\n } as ServerSentEvent);\n }\n } catch (error) {\n controller.error(\n error instanceof Error ? error : new Error(String(error)),\n );\n } finally {\n // Reset state\n currentEvent.event = DEFAULT_EVENT_TYPE;\n currentEvent.id = undefined;\n currentEvent.retry = undefined;\n currentEvent.data = [];\n }\n }\n}\n\n/**\n * A TransformStream that converts a stream of strings into a stream of ServerSentEvent objects.\n */\nexport class ServerSentEventTransformStream extends TransformStream<\n string,\n ServerSentEvent\n> {\n constructor() {\n super(new ServerSentEventTransformer());\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { TextLineTransformStream } from './textLineTransformStream';\nimport {\n ServerSentEvent,\n ServerSentEventTransformStream,\n} from './serverSentEventTransformStream';\n\n/**\n * A ReadableStream of ServerSentEvent objects.\n */\nexport type ServerSentEventStream = ReadableStream<ServerSentEvent>;\n\n/**\n * Converts a Response object to a ServerSentEventStream.\n *\n * Processes the response body through a series of transform streams:\n * 1. TextDecoderStream: Decode Uint8Array data to UTF-8 strings\n * 2. TextLineStream: Split text by lines\n * 3. ServerSentEventStream: Parse line data into server-sent events\n *\n * @param response - The Response object to convert\n * @returns A ReadableStream of ServerSentEvent objects\n * @throws Error if the response body is null\n */\nexport function toServerSentEventStream(\n response: Response,\n): ServerSentEventStream {\n if (!response.body) {\n throw new Error('Response body is null');\n }\n\n return response.body\n .pipeThrough(new TextDecoderStream('utf-8'))\n .pipeThrough(new TextLineTransformStream())\n .pipeThrough(new ServerSentEventTransformStream());\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ServerSentEvent } from './serverSentEventTransformStream';\nimport { ServerSentEventStream } from './eventStreamConverter';\n\nexport interface JsonServerSentEvent<DATA> extends Omit<ServerSentEvent, 'data'> {\n data: DATA;\n}\n\nexport class JsonServerSentEventTransform<DATA> implements Transformer<ServerSentEvent, JsonServerSentEvent<DATA>> {\n transform(\n chunk: ServerSentEvent,\n controller: TransformStreamDefaultController<JsonServerSentEvent<DATA>>,\n ) {\n const json = JSON.parse(chunk.data) as DATA;\n controller.enqueue({\n data: json,\n event: chunk.event,\n id: chunk.id,\n retry: chunk.retry,\n });\n }\n}\n\nexport class JsonServerSentEventTransformStream<DATA> extends TransformStream<ServerSentEvent, JsonServerSentEvent<DATA>> {\n constructor() {\n super(new JsonServerSentEventTransform());\n }\n}\n\nexport type JsonServerSentEventStream<DATA> = ReadableStream<JsonServerSentEvent<DATA>>;\n\nexport function toJsonServerSentEventStream<DATA>(\n serverSentEventStream: ServerSentEventStream,\n): JsonServerSentEventStream<DATA> {\n return serverSentEventStream.pipeThrough(new JsonServerSentEventTransformStream<DATA>());\n}","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { toServerSentEventStream } from './eventStreamConverter';\nimport {\n ContentTypeHeader,\n ContentTypeValues,\n FetchExchange,\n ResponseInterceptor,\n} from '@ahoo-wang/fetcher';\nimport { toJsonServerSentEventStream } from './jsonServerSentEventTransformStream';\n\n/**\n * The name of the EventStreamInterceptor.\n */\nexport const EVENT_STREAM_INTERCEPTOR_NAME = 'EventStreamInterceptor';\n\n/**\n * The order of the EventStreamInterceptor.\n * Set to Number.MAX_SAFE_INTEGER - 1000 to ensure it runs latest among response interceptors.\n */\nexport const EVENT_STREAM_INTERCEPTOR_ORDER = Number.MAX_SAFE_INTEGER - 1000;\n\n/**\n * Interceptor that enhances Response objects with event stream capabilities.\n *\n * This interceptor detects responses with `text/event-stream` content type and adds\n * an `eventStream()` method to the Response object, which returns a readable stream\n * of Server-Sent Events that can be consumed using `for await` syntax.\n *\n * @remarks\n * This interceptor runs at the very end of the response interceptor chain to ensure\n * it runs after all standard response processing is complete, as it adds\n * specialized functionality to the response object. The order is set to\n * EVENT_STREAM_INTERCEPTOR_ORDER to ensure it executes latest among response interceptors,\n * allowing for other response interceptors to run before it if needed. This positioning\n * ensures that all response processing is completed before specialized event stream\n * functionality is added to the response object.\n *\n * @example\n * ```typescript\n * // Using the eventStream method\n * const response = await fetcher.get('/events');\n * if (response.headers.get('content-type')?.includes('text/event-stream')) {\n * const eventStream = response.eventStream();\n * for await (const event of eventStream) {\n * console.log('Received event:', event);\n * }\n * }\n * ```\n */\nexport class EventStreamInterceptor implements ResponseInterceptor {\n readonly name = EVENT_STREAM_INTERCEPTOR_NAME;\n readonly order = EVENT_STREAM_INTERCEPTOR_ORDER;\n\n /**\n * Intercepts responses to add event stream capabilities.\n *\n * This method runs at the very end of the response interceptor chain to ensure\n * it runs after all standard response processing is complete. It detects responses\n * with `text/event-stream` content type and adds an `eventStream()` method to\n * the Response object, which returns a readable stream of Server-Sent Events.\n *\n * @param exchange - The exchange containing the response to enhance\n *\n * @remarks\n * This method executes latest among response interceptors to ensure all response\n * processing is completed before specialized event stream functionality is added.\n * It only enhances responses with `text/event-stream` content type, leaving other\n * responses unchanged. The positioning at the end of the response chain ensures\n * that all response transformations and validations are completed before event\n * stream capabilities are added to the response object.\n */\n intercept(exchange: FetchExchange) {\n // Check if the response is an event stream\n const response = exchange.response;\n if (!response) {\n return;\n }\n const contentType = response.headers.get(ContentTypeHeader);\n if (contentType?.includes(ContentTypeValues.TEXT_EVENT_STREAM)) {\n response.eventStream = () => toServerSentEventStream(response);\n response.jsonEventStream = () => toJsonServerSentEventStream(toServerSentEventStream(response));\n }\n }\n}\n"],"names":["TextLineTransformer","chunk","controller","lines","line","error","TextLineTransformStream","ServerSentEventField","processFieldInternal","field","value","currentEvent","retryValue","DEFAULT_EVENT_TYPE","ServerSentEventTransformer","colonIndex","ServerSentEventTransformStream","toServerSentEventStream","response","JsonServerSentEventTransform","json","JsonServerSentEventTransformStream","toJsonServerSentEventStream","serverSentEventStream","EVENT_STREAM_INTERCEPTOR_NAME","EVENT_STREAM_INTERCEPTOR_ORDER","EventStreamInterceptor","exchange","ContentTypeHeader","ContentTypeValues"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.umd.js","sources":["../src/textLineTransformStream.ts","../src/serverSentEventTransformStream.ts","../src/eventStreamConverter.ts","../src/jsonServerSentEventTransformStream.ts","../src/eventStreamInterceptor.ts"],"sourcesContent":["/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Transformer that splits text into lines.\n *\n * This transformer accumulates chunks of text and splits them by newline characters,\n * emitting each line as a separate chunk while preserving the remaining buffer\n * for the next chunk.\n */\nexport class TextLineTransformer implements Transformer<string, string> {\n private buffer = '';\n\n /**\n * Transform input string chunk by splitting it into lines.\n *\n * @param chunk Input string chunk\n * @param controller Controller for controlling the transform stream\n */\n transform(\n chunk: string,\n controller: TransformStreamDefaultController<string>,\n ) {\n try {\n this.buffer += chunk;\n const lines = this.buffer.split('\\n');\n this.buffer = lines.pop() || '';\n\n for (const line of lines) {\n controller.enqueue(line);\n }\n } catch (error) {\n controller.error(error);\n }\n }\n\n /**\n * Flush remaining buffer when the stream ends.\n *\n * @param controller Controller for controlling the transform stream\n */\n flush(controller: TransformStreamDefaultController<string>) {\n try {\n // Only send when buffer is not empty, avoid sending meaningless empty lines\n if (this.buffer) {\n controller.enqueue(this.buffer);\n }\n } catch (error) {\n controller.error(error);\n }\n }\n}\n\n/**\n * A TransformStream that splits text into lines.\n */\nexport class TextLineTransformStream extends TransformStream<string, string> {\n constructor() {\n super(new TextLineTransformer());\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Represents a message sent in an event stream.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format}\n */\nexport interface ServerSentEvent {\n /** The event ID to set the EventSource object's last event ID value. */\n id?: string;\n /** A string identifying the type of event described. */\n event: string;\n /** The event data */\n data: string;\n /** The reconnection interval (in milliseconds) to wait before retrying the connection */\n retry?: number;\n}\n\nexport enum ServerSentEventField {\n ID = 'id',\n RETRY = 'retry',\n EVENT = 'event',\n DATA = 'data',\n}\n\n/**\n * Process field value\n * @param field Field name\n * @param value Field value\n * @param currentEvent Current event state\n */\nfunction processFieldInternal(\n field: string,\n value: string,\n currentEvent: EventState,\n) {\n switch (field) {\n case ServerSentEventField.EVENT:\n currentEvent.event = value;\n break;\n case ServerSentEventField.DATA:\n currentEvent.data.push(value);\n break;\n case ServerSentEventField.ID:\n currentEvent.id = value;\n break;\n case ServerSentEventField.RETRY: {\n const retryValue = parseInt(value, 10);\n if (!isNaN(retryValue)) {\n currentEvent.retry = retryValue;\n }\n break;\n }\n default:\n // Ignore unknown fields\n break;\n }\n}\n\ninterface EventState {\n event?: string;\n id?: string;\n retry?: number;\n data: string[];\n}\n\nconst DEFAULT_EVENT_TYPE = 'message';\n\n/**\n * Transformer responsible for converting a string stream into a ServerSentEvent object stream.\n *\n * Implements the Transformer interface for processing data transformation in TransformStream.\n */\nexport class ServerSentEventTransformer\n implements Transformer<string, ServerSentEvent>\n{\n // Initialize currentEvent with default values in a closure\n private currentEvent: EventState = {\n event: DEFAULT_EVENT_TYPE,\n id: undefined,\n retry: undefined,\n data: [],\n };\n\n /**\n * Transform input string chunk into ServerSentEvent object.\n *\n * @param chunk Input string chunk\n * @param controller Controller for controlling the transform stream\n */\n transform(\n chunk: string,\n controller: TransformStreamDefaultController<ServerSentEvent>,\n ) {\n const currentEvent = this.currentEvent;\n try {\n // Skip empty lines (event separator)\n if (chunk.trim() === '') {\n // If there is accumulated event data, send event\n if (currentEvent.data.length > 0) {\n controller.enqueue({\n event: currentEvent.event || DEFAULT_EVENT_TYPE,\n data: currentEvent.data.join('\\n'),\n id: currentEvent.id || '',\n retry: currentEvent.retry,\n } as ServerSentEvent);\n\n // Reset current event (preserve id and retry for subsequent events)\n currentEvent.event = DEFAULT_EVENT_TYPE;\n // Preserve id and retry for subsequent events (no need to reassign to themselves)\n currentEvent.data = [];\n }\n return;\n }\n\n // Ignore comment lines (starting with colon)\n if (chunk.startsWith(':')) {\n return;\n }\n\n // Parse fields\n const colonIndex = chunk.indexOf(':');\n let field: string;\n let value: string;\n\n if (colonIndex === -1) {\n // No colon, entire line as field name, value is empty\n field = chunk.toLowerCase();\n value = '';\n } else {\n // Extract field name and value\n field = chunk.substring(0, colonIndex).toLowerCase();\n value = chunk.substring(colonIndex + 1);\n\n // If value starts with space, remove leading space\n if (value.startsWith(' ')) {\n value = value.substring(1);\n }\n }\n\n // Remove trailing newlines from field and value\n field = field.trim();\n value = value.trim();\n\n processFieldInternal(field, value, currentEvent);\n } catch (error) {\n controller.error(\n error instanceof Error ? error : new Error(String(error)),\n );\n // Reset state\n currentEvent.event = DEFAULT_EVENT_TYPE;\n currentEvent.id = undefined;\n currentEvent.retry = undefined;\n currentEvent.data = [];\n }\n }\n\n /**\n * Called when the stream ends, used to process remaining data.\n *\n * @param controller Controller for controlling the transform stream\n */\n flush(controller: TransformStreamDefaultController<ServerSentEvent>) {\n const currentEvent = this.currentEvent;\n try {\n // Send the last event (if any)\n if (currentEvent.data.length > 0) {\n controller.enqueue({\n event: currentEvent.event || DEFAULT_EVENT_TYPE,\n data: currentEvent.data.join('\\n'),\n id: currentEvent.id || '',\n retry: currentEvent.retry,\n } as ServerSentEvent);\n }\n } catch (error) {\n controller.error(\n error instanceof Error ? error : new Error(String(error)),\n );\n } finally {\n // Reset state\n currentEvent.event = DEFAULT_EVENT_TYPE;\n currentEvent.id = undefined;\n currentEvent.retry = undefined;\n currentEvent.data = [];\n }\n }\n}\n\n/**\n * A TransformStream that converts a stream of strings into a stream of ServerSentEvent objects.\n */\nexport class ServerSentEventTransformStream extends TransformStream<\n string,\n ServerSentEvent\n> {\n constructor() {\n super(new ServerSentEventTransformer());\n }\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { TextLineTransformStream } from './textLineTransformStream';\nimport {\n ServerSentEvent,\n ServerSentEventTransformStream,\n} from './serverSentEventTransformStream';\n\n/**\n * A ReadableStream of ServerSentEvent objects.\n */\nexport type ServerSentEventStream = ReadableStream<ServerSentEvent>;\n\n/**\n * Converts a Response object to a ServerSentEventStream.\n *\n * Processes the response body through a series of transform streams:\n * 1. TextDecoderStream: Decode Uint8Array data to UTF-8 strings\n * 2. TextLineStream: Split text by lines\n * 3. ServerSentEventStream: Parse line data into server-sent events\n *\n * @param response - The Response object to convert\n * @returns A ReadableStream of ServerSentEvent objects\n * @throws Error if the response body is null\n */\nexport function toServerSentEventStream(\n response: Response,\n): ServerSentEventStream {\n if (!response.body) {\n throw new Error('Response body is null');\n }\n\n return response.body\n .pipeThrough(new TextDecoderStream('utf-8'))\n .pipeThrough(new TextLineTransformStream())\n .pipeThrough(new ServerSentEventTransformStream());\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ServerSentEvent } from './serverSentEventTransformStream';\nimport { ServerSentEventStream } from './eventStreamConverter';\n\nexport interface JsonServerSentEvent<DATA>\n extends Omit<ServerSentEvent, 'data'> {\n data: DATA;\n}\n\nexport class JsonServerSentEventTransform<DATA>\n implements Transformer<ServerSentEvent, JsonServerSentEvent<DATA>> {\n transform(\n chunk: ServerSentEvent,\n controller: TransformStreamDefaultController<JsonServerSentEvent<DATA>>,\n ) {\n const json = JSON.parse(chunk.data) as DATA;\n controller.enqueue({\n data: json,\n event: chunk.event,\n id: chunk.id,\n retry: chunk.retry,\n });\n }\n}\n\nexport class JsonServerSentEventTransformStream<DATA> extends TransformStream<\n ServerSentEvent,\n JsonServerSentEvent<DATA>\n> {\n constructor() {\n super(new JsonServerSentEventTransform());\n }\n}\n\nexport type JsonServerSentEventStream<DATA> = ReadableStream<\n JsonServerSentEvent<DATA>\n>;\n\nexport function toJsonServerSentEventStream<DATA>(\n serverSentEventStream: ServerSentEventStream,\n): JsonServerSentEventStream<DATA> {\n return serverSentEventStream.pipeThrough(\n new JsonServerSentEventTransformStream<DATA>(),\n );\n}\n","/*\n * Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { toServerSentEventStream } from './eventStreamConverter';\nimport {\n ContentTypeHeader,\n ContentTypeValues,\n FetchExchange,\n ResponseInterceptor,\n} from '@ahoo-wang/fetcher';\nimport { toJsonServerSentEventStream } from './jsonServerSentEventTransformStream';\n\n/**\n * The name of the EventStreamInterceptor.\n */\nexport const EVENT_STREAM_INTERCEPTOR_NAME = 'EventStreamInterceptor';\n\n/**\n * The order of the EventStreamInterceptor.\n * Set to Number.MAX_SAFE_INTEGER - 1000 to ensure it runs latest among response interceptors.\n */\nexport const EVENT_STREAM_INTERCEPTOR_ORDER = Number.MAX_SAFE_INTEGER - 1000;\n\n/**\n * Interceptor that enhances Response objects with event stream capabilities.\n *\n * This interceptor detects responses with `text/event-stream` content type and adds\n * an `eventStream()` method to the Response object, which returns a readable stream\n * of Server-Sent Events that can be consumed using `for await` syntax.\n *\n * @remarks\n * This interceptor runs at the very end of the response interceptor chain to ensure\n * it runs after all standard response processing is complete, as it adds\n * specialized functionality to the response object. The order is set to\n * EVENT_STREAM_INTERCEPTOR_ORDER to ensure it executes latest among response interceptors,\n * allowing for other response interceptors to run before it if needed. This positioning\n * ensures that all response processing is completed before specialized event stream\n * functionality is added to the response object.\n *\n * @example\n * ```typescript\n * // Using the eventStream method\n * const response = await fetcher.get('/events');\n * if (response.headers.get('content-type')?.includes('text/event-stream')) {\n * const eventStream = response.eventStream();\n * for await (const event of eventStream) {\n * console.log('Received event:', event);\n * }\n * }\n * ```\n */\nexport class EventStreamInterceptor implements ResponseInterceptor {\n readonly name = EVENT_STREAM_INTERCEPTOR_NAME;\n readonly order = EVENT_STREAM_INTERCEPTOR_ORDER;\n\n /**\n * Intercepts responses to add event stream capabilities.\n *\n * This method runs at the very end of the response interceptor chain to ensure\n * it runs after all standard response processing is complete. It detects responses\n * with `text/event-stream` content type and adds an `eventStream()` method to\n * the Response object, which returns a readable stream of Server-Sent Events.\n *\n * @param exchange - The exchange containing the response to enhance\n *\n * @remarks\n * This method executes latest among response interceptors to ensure all response\n * processing is completed before specialized event stream functionality is added.\n * It only enhances responses with `text/event-stream` content type, leaving other\n * responses unchanged. The positioning at the end of the response chain ensures\n * that all response transformations and validations are completed before event\n * stream capabilities are added to the response object.\n */\n intercept(exchange: FetchExchange) {\n // Check if the response is an event stream\n const response = exchange.response;\n if (!response) {\n return;\n }\n const contentType = response.headers.get(ContentTypeHeader);\n if (contentType?.includes(ContentTypeValues.TEXT_EVENT_STREAM)) {\n response.eventStream = () => toServerSentEventStream(response);\n response.jsonEventStream = () =>\n toJsonServerSentEventStream(toServerSentEventStream(response));\n }\n }\n}\n"],"names":["TextLineTransformer","chunk","controller","lines","line","error","TextLineTransformStream","ServerSentEventField","processFieldInternal","field","value","currentEvent","retryValue","DEFAULT_EVENT_TYPE","ServerSentEventTransformer","colonIndex","ServerSentEventTransformStream","toServerSentEventStream","response","JsonServerSentEventTransform","json","JsonServerSentEventTransformStream","toJsonServerSentEventStream","serverSentEventStream","EVENT_STREAM_INTERCEPTOR_NAME","EVENT_STREAM_INTERCEPTOR_ORDER","EventStreamInterceptor","exchange","ContentTypeHeader","ContentTypeValues"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonServerSentEventTransformStream.d.ts","sourceRoot":"","sources":["../src/jsonServerSentEventTransformStream.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,mBAAmB,CAAC,IAAI,
|
|
1
|
+
{"version":3,"file":"jsonServerSentEventTransformStream.d.ts","sourceRoot":"","sources":["../src/jsonServerSentEventTransformStream.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,mBAAmB,CAAC,IAAI,CACvC,SAAQ,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC;IACrC,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,qBAAa,4BAA4B,CAAC,IAAI,CAC5C,YAAW,WAAW,CAAC,eAAe,EAAE,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAClE,SAAS,CACP,KAAK,EAAE,eAAe,EACtB,UAAU,EAAE,gCAAgC,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;CAU1E;AAED,qBAAa,kCAAkC,CAAC,IAAI,CAAE,SAAQ,eAAe,CAC3E,eAAe,EACf,mBAAmB,CAAC,IAAI,CAAC,CAC1B;;CAIA;AAED,MAAM,MAAM,yBAAyB,CAAC,IAAI,IAAI,cAAc,CAC1D,mBAAmB,CAAC,IAAI,CAAC,CAC1B,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,IAAI,EAC9C,qBAAqB,EAAE,qBAAqB,GAC3C,yBAAyB,CAAC,IAAI,CAAC,CAIjC"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahoo-wang/fetcher-eventstream",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Server-Sent Events (SSE) support for Fetcher HTTP client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Server-Sent Events (SSE) support for Fetcher HTTP client with native LLM streaming API support. Enables real-time data streaming and token-by-token LLM response handling.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fetch",
|
|
7
7
|
"event-stream",
|
|
8
8
|
"sse",
|
|
9
|
-
"server-sent-events"
|
|
9
|
+
"server-sent-events",
|
|
10
|
+
"real-time",
|
|
11
|
+
"streaming",
|
|
12
|
+
"llm",
|
|
13
|
+
"openai",
|
|
14
|
+
"claude"
|
|
10
15
|
],
|
|
11
16
|
"author": "Ahoo-Wang",
|
|
12
17
|
"license": "Apache-2.0",
|
|
@@ -34,7 +39,7 @@
|
|
|
34
39
|
"dist"
|
|
35
40
|
],
|
|
36
41
|
"dependencies": {
|
|
37
|
-
"@ahoo-wang/fetcher": "0.
|
|
42
|
+
"@ahoo-wang/fetcher": "1.0.0"
|
|
38
43
|
},
|
|
39
44
|
"devDependencies": {
|
|
40
45
|
"@vitest/coverage-v8": "^3.2.4",
|