sse-line-parser 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +103 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# SSE Stream Parser
|
|
2
|
+
|
|
3
|
+
一个**轻量、纯粹、可用于生产环境的 SSE(Server-Sent Events)流解析工具**,基于标准的 SSE 协议实现,与具体应用场景无关,可广泛应用于各类 SSE 数据流解析,不掺杂请求管理、不中断连接、只做一件事:**把流解析干净**。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ✨ 特性亮点
|
|
8
|
+
|
|
9
|
+
- ✅ **纯 SSE 流解析**,不关心请求生命周期
|
|
10
|
+
- ✅ 支持标准 `data:` 协议格式,兼容所有基于 SSE 的服务
|
|
11
|
+
- ✅ 内置 `[DONE]` 提前识别与快速跳过
|
|
12
|
+
- ✅ 低 GC 压力,避免不必要对象创建
|
|
13
|
+
- ✅ 可运行于 **Node.js ≥ 18**(原生 `fetch` + `ReadableStream`)
|
|
14
|
+
- ✅ TypeScript 原生支持,类型清晰
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 📦 安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add sse-stream-parser
|
|
22
|
+
# or
|
|
23
|
+
npm install sse-stream-parser
|
|
24
|
+
# or
|
|
25
|
+
yarn add sse-stream-parser
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🧠 设计理念
|
|
31
|
+
|
|
32
|
+
> **只做流解析,不做控制逻辑**
|
|
33
|
+
|
|
34
|
+
本插件**不会**:
|
|
35
|
+
|
|
36
|
+
- ❌ 管理请求中断 / abort
|
|
37
|
+
- ❌ 包装 fetch
|
|
38
|
+
- ❌ 维护连接状态
|
|
39
|
+
- ❌ 引入 EventEmitter / Rx / class 抽象
|
|
40
|
+
|
|
41
|
+
本插件**只负责**:
|
|
42
|
+
|
|
43
|
+
- ✔ 解析 `ReadableStreamDefaultReader<Uint8Array<ArrayBuffer>>`
|
|
44
|
+
- ✔ 拆分 SSE 行
|
|
45
|
+
- ✔ 解析 `data:` 内容
|
|
46
|
+
- ✔ 识别 `[DONE]`
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 🚀 基本用法
|
|
51
|
+
|
|
52
|
+
### 1️⃣ 基础示例(Node.js / Edge)
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { parseSSEStream } from "sse-stream-parser";
|
|
56
|
+
|
|
57
|
+
const res = await fetch(url, options);
|
|
58
|
+
|
|
59
|
+
if (!res.body) return;
|
|
60
|
+
|
|
61
|
+
const reader = res.body.getReader();
|
|
62
|
+
|
|
63
|
+
await parseSSEStream({
|
|
64
|
+
renderStream: reader,
|
|
65
|
+
options: {
|
|
66
|
+
onMessage(data) {
|
|
67
|
+
// data 是解析后的 SSE 消息
|
|
68
|
+
console.log(data);
|
|
69
|
+
},
|
|
70
|
+
onDone() {
|
|
71
|
+
console.log("stream finished");
|
|
72
|
+
},
|
|
73
|
+
onError(err) {
|
|
74
|
+
console.error("Error reading stream:", err);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 🔍 `[DONE]` 的处理逻辑
|
|
83
|
+
|
|
84
|
+
插件内部对以下情况做了优化:
|
|
85
|
+
|
|
86
|
+
```txt
|
|
87
|
+
data: [DONE]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- 提前识别 `[DONE]`
|
|
91
|
+
- **不再进入 JSON.parse**
|
|
92
|
+
- 立即触发 `onDone`
|
|
93
|
+
- 后续数据直接跳过
|
|
94
|
+
|
|
95
|
+
避免无意义的解析和异常捕获。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## ⚙️ API 说明
|
|
100
|
+
|
|
101
|
+
### `parseSSEStream(options)`
|
|
102
|
+
|
|
103
|
+
#### 参数
|
|
104
|
+
|
|
105
|
+
| 参数 | 类型 | 说明 |
|
|
106
|
+
| ------------------- | ----------------------------------------- | ---------------------------- |
|
|
107
|
+
| `renderStream` | `ReadableStreamDefaultReader<Uint8Array>` | SSE 响应体的 Reader |
|
|
108
|
+
| `options` | `StreamOptions` | 包含回调函数的选项对象 |
|
|
109
|
+
| `options.onMessage` | `(data: T) => void` | 每条消息回调 |
|
|
110
|
+
| `options.onDone` | `() => void` | 收到 `[DONE]` 时触发(可选) |
|
|
111
|
+
| `options.onError` | `(err: Error) => void` | 解析错误回调(可选) |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🌍 运行环境
|
|
116
|
+
|
|
117
|
+
- Node.js **>= 18**
|
|
118
|
+
- Bun / Deno / Edge Runtime
|
|
119
|
+
- 浏览器(需要支持 `ReadableStream`)
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 🧱 适用场景
|
|
124
|
+
|
|
125
|
+
- OpenAI / Claude / Gemini 等 AI 服务 SSE
|
|
126
|
+
- 实时数据推送服务
|
|
127
|
+
- 股票行情、天气数据等实时更新
|
|
128
|
+
- 日志流实时监控
|
|
129
|
+
- 自定义 SSE 服务
|
|
130
|
+
- Web / Node / Edge 流式消费
|
|
131
|
+
- Infra / SDK / 中间层
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 📜 License
|
|
136
|
+
|
|
137
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple parser for Server-Sent Events (SSE) streams
|
|
3
|
+
*/
|
|
4
|
+
type SSEMessage = {
|
|
5
|
+
event?: string;
|
|
6
|
+
data: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
retry?: number;
|
|
9
|
+
};
|
|
10
|
+
type LineData = {
|
|
11
|
+
id?: number;
|
|
12
|
+
event?: string;
|
|
13
|
+
data: Record<string, any> | string | null;
|
|
14
|
+
error?: string;
|
|
15
|
+
};
|
|
16
|
+
type StreamOptions<T = any> = {
|
|
17
|
+
onMessage: (data: T) => void;
|
|
18
|
+
onDone?: () => void;
|
|
19
|
+
onError?: (err: Error) => void;
|
|
20
|
+
};
|
|
21
|
+
type SSEProps = {
|
|
22
|
+
renderStream: ReadableStreamDefaultReader<Uint8Array<ArrayBuffer>>;
|
|
23
|
+
options: StreamOptions;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
declare function parseSSEStream<T = any>({ renderStream, options, }: SSEProps): Promise<void>;
|
|
27
|
+
|
|
28
|
+
export { type LineData, type SSEMessage, parseSSEStream as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple parser for Server-Sent Events (SSE) streams
|
|
3
|
+
*/
|
|
4
|
+
type SSEMessage = {
|
|
5
|
+
event?: string;
|
|
6
|
+
data: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
retry?: number;
|
|
9
|
+
};
|
|
10
|
+
type LineData = {
|
|
11
|
+
id?: number;
|
|
12
|
+
event?: string;
|
|
13
|
+
data: Record<string, any> | string | null;
|
|
14
|
+
error?: string;
|
|
15
|
+
};
|
|
16
|
+
type StreamOptions<T = any> = {
|
|
17
|
+
onMessage: (data: T) => void;
|
|
18
|
+
onDone?: () => void;
|
|
19
|
+
onError?: (err: Error) => void;
|
|
20
|
+
};
|
|
21
|
+
type SSEProps = {
|
|
22
|
+
renderStream: ReadableStreamDefaultReader<Uint8Array<ArrayBuffer>>;
|
|
23
|
+
options: StreamOptions;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
declare function parseSSEStream<T = any>({ renderStream, options, }: SSEProps): Promise<void>;
|
|
27
|
+
|
|
28
|
+
export { type LineData, type SSEMessage, parseSSEStream as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
default: () => index_default
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/parseLine.ts
|
|
28
|
+
var lineData = { data: null };
|
|
29
|
+
var dataBuffer = [];
|
|
30
|
+
function parseLine(line) {
|
|
31
|
+
if (line === "") {
|
|
32
|
+
if (dataBuffer.length === 0) return null;
|
|
33
|
+
const raw = dataBuffer.join("\n");
|
|
34
|
+
let data = raw;
|
|
35
|
+
const first = raw[0];
|
|
36
|
+
if (first === "{" || first === "[") {
|
|
37
|
+
try {
|
|
38
|
+
data = JSON.parse(raw);
|
|
39
|
+
} catch {
|
|
40
|
+
data = raw;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const result = {
|
|
44
|
+
id: lineData.id,
|
|
45
|
+
event: lineData.event,
|
|
46
|
+
data
|
|
47
|
+
};
|
|
48
|
+
lineData = { data: null };
|
|
49
|
+
dataBuffer.length = 0;
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
if (line.startsWith(":")) return null;
|
|
53
|
+
const colon = line.indexOf(":");
|
|
54
|
+
let field = "";
|
|
55
|
+
let value = "";
|
|
56
|
+
if (colon === -1) {
|
|
57
|
+
field = line;
|
|
58
|
+
} else {
|
|
59
|
+
field = line.slice(0, colon);
|
|
60
|
+
value = line.slice(colon + 1);
|
|
61
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
62
|
+
}
|
|
63
|
+
switch (field) {
|
|
64
|
+
case "id":
|
|
65
|
+
lineData.id = Number(value);
|
|
66
|
+
break;
|
|
67
|
+
case "event":
|
|
68
|
+
lineData.event = value;
|
|
69
|
+
break;
|
|
70
|
+
case "data":
|
|
71
|
+
dataBuffer.push(value);
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/stream.ts
|
|
80
|
+
var queue = [];
|
|
81
|
+
async function parseSSEStream({
|
|
82
|
+
renderStream,
|
|
83
|
+
options
|
|
84
|
+
}) {
|
|
85
|
+
const decoder = new TextDecoder();
|
|
86
|
+
while (true) {
|
|
87
|
+
try {
|
|
88
|
+
const { value, done } = await renderStream.read();
|
|
89
|
+
if (done) break;
|
|
90
|
+
const text = decoder.decode(value);
|
|
91
|
+
const lines = text.split("\n");
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
const lineVal = line.trimEnd();
|
|
94
|
+
let msg;
|
|
95
|
+
try {
|
|
96
|
+
msg = parseLine(lineVal);
|
|
97
|
+
if (msg && msg.data === "[DONE]") {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (options.onError) {
|
|
102
|
+
options.onError(new Error(`Failed to parse line: ${lineVal}`));
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (msg && msg.data != null) {
|
|
107
|
+
queue.push(msg);
|
|
108
|
+
options.onMessage(msg);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error("Error reading stream:", error);
|
|
113
|
+
if (options.onError) {
|
|
114
|
+
options.onError(error);
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (options.onDone) {
|
|
120
|
+
options.onDone();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/index.ts
|
|
125
|
+
var index_default = parseSSEStream;
|
|
126
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/parseLine.ts","../src/stream.ts"],"sourcesContent":["export type { SSEMessage, LineData } from \"./types\";\nimport { parseSSEStream } from \"./stream\";\n\nexport default parseSSEStream;\n","// Define the structure for parsed Server-Sent Event (SSE) line data\nexport type LineData = {\n id?: number;\n event?: string;\n data: Record<string, any> | string | null;\n error?: string;\n};\n\n// Global state variables to maintain parsing context across multiple lines\nlet lineData: LineData = { data: null }; // Stores event metadata (id, event type)\nlet dataBuffer: string[] = []; // Accumulates multi-line data segments\n\n/**\n * Parses a single line of Server-Sent Events (SSE) protocol\n *\n * Handles SSE fields: data, event, id, and comments.\n * Returns complete event data when an empty line is encountered,\n * otherwise updates internal state and returns null.\n */\nexport function parseLine(line: string): LineData | null {\n // Empty line indicates the end of an event - process and return accumulated data\n if (line === \"\") {\n if (dataBuffer.length === 0) return null;\n\n const raw = dataBuffer.join(\"\\n\");\n\n // Initialize data as raw string, attempt JSON parsing if it looks like JSON\n let data: any = raw;\n const first = raw[0];\n if (first === \"{\" || first === \"[\") {\n try {\n data = JSON.parse(raw);\n } catch {\n data = raw;\n }\n }\n\n // Create result with accumulated event data\n const result: LineData = {\n id: lineData.id,\n event: lineData.event,\n data,\n };\n\n // Reset state for next event\n lineData = { data: null };\n dataBuffer.length = 0;\n return result;\n }\n\n // Skip comment lines (start with ':')\n if (line.startsWith(\":\")) return null;\n\n // Parse field-value pairs (format: \"field: value\")\n const colon = line.indexOf(\":\");\n let field = \"\";\n let value = \"\";\n\n if (colon === -1) {\n field = line;\n } else {\n field = line.slice(0, colon);\n value = line.slice(colon + 1);\n if (value.startsWith(\" \")) value = value.slice(1);\n }\n\n // Update internal state based on field type\n switch (field) {\n case \"id\":\n lineData.id = Number(value);\n break;\n case \"event\":\n lineData.event = value;\n break;\n case \"data\":\n // Multi-line data segments are appended to buffer\n dataBuffer.push(value);\n break;\n default:\n // Ignore unknown fields\n break;\n }\n\n return null;\n}\n","import type { LineData, SSEProps } from \"./types\";\nimport { parseLine } from \"./parseLine\";\n\n// Queue to store parsed line data\nconst queue = [] as LineData[];\nexport async function parseSSEStream<T = any>({\n renderStream,\n options,\n}: SSEProps) {\n const decoder = new TextDecoder();\n while (true) {\n try {\n const { value, done } = await renderStream.read();\n if (done) break;\n\n // Decode and split text into lines\n const text = decoder.decode(value);\n const lines = text.split(\"\\n\");\n\n for (const line of lines) {\n const lineVal = line.trimEnd();\n\n let msg;\n try {\n msg = parseLine(lineVal);\n // Check for DONE flag and terminate if found\n if (msg && msg.data === \"[DONE]\") {\n return;\n }\n } catch (error) {\n if (options.onError) {\n options.onError(new Error(`Failed to parse line: ${lineVal}`));\n }\n continue;\n }\n\n // Process non-null messages\n if (msg && msg.data != null) {\n queue.push(msg);\n options.onMessage(msg);\n }\n }\n } catch (error) {\n console.error(\"Error reading stream:\", error);\n if (options.onError) {\n options.onError(error as Error);\n }\n break;\n }\n }\n // Call completion handler if provided\n if (options.onDone) {\n options.onDone();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSA,IAAI,WAAqB,EAAE,MAAM,KAAK;AACtC,IAAI,aAAuB,CAAC;AASrB,SAAS,UAAU,MAA+B;AAEvD,MAAI,SAAS,IAAI;AACf,QAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,UAAM,MAAM,WAAW,KAAK,IAAI;AAGhC,QAAI,OAAY;AAChB,UAAM,QAAQ,IAAI,CAAC;AACnB,QAAI,UAAU,OAAO,UAAU,KAAK;AAClC,UAAI;AACF,eAAO,KAAK,MAAM,GAAG;AAAA,MACvB,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAGA,UAAM,SAAmB;AAAA,MACvB,IAAI,SAAS;AAAA,MACb,OAAO,SAAS;AAAA,MAChB;AAAA,IACF;AAGA,eAAW,EAAE,MAAM,KAAK;AACxB,eAAW,SAAS;AACpB,WAAO;AAAA,EACT;AAGA,MAAI,KAAK,WAAW,GAAG,EAAG,QAAO;AAGjC,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,MAAI,QAAQ;AACZ,MAAI,QAAQ;AAEZ,MAAI,UAAU,IAAI;AAChB,YAAQ;AAAA,EACV,OAAO;AACL,YAAQ,KAAK,MAAM,GAAG,KAAK;AAC3B,YAAQ,KAAK,MAAM,QAAQ,CAAC;AAC5B,QAAI,MAAM,WAAW,GAAG,EAAG,SAAQ,MAAM,MAAM,CAAC;AAAA,EAClD;AAGA,UAAQ,OAAO;AAAA,IACb,KAAK;AACH,eAAS,KAAK,OAAO,KAAK;AAC1B;AAAA,IACF,KAAK;AACH,eAAS,QAAQ;AACjB;AAAA,IACF,KAAK;AAEH,iBAAW,KAAK,KAAK;AACrB;AAAA,IACF;AAEE;AAAA,EACJ;AAEA,SAAO;AACT;;;AChFA,IAAM,QAAQ,CAAC;AACf,eAAsB,eAAwB;AAAA,EAC5C;AAAA,EACA;AACF,GAAa;AACX,QAAM,UAAU,IAAI,YAAY;AAChC,SAAO,MAAM;AACX,QAAI;AACF,YAAM,EAAE,OAAO,KAAK,IAAI,MAAM,aAAa,KAAK;AAChD,UAAI,KAAM;AAGV,YAAM,OAAO,QAAQ,OAAO,KAAK;AACjC,YAAM,QAAQ,KAAK,MAAM,IAAI;AAE7B,iBAAW,QAAQ,OAAO;AACxB,cAAM,UAAU,KAAK,QAAQ;AAE7B,YAAI;AACJ,YAAI;AACF,gBAAM,UAAU,OAAO;AAEvB,cAAI,OAAO,IAAI,SAAS,UAAU;AAChC;AAAA,UACF;AAAA,QACF,SAAS,OAAO;AACd,cAAI,QAAQ,SAAS;AACnB,oBAAQ,QAAQ,IAAI,MAAM,yBAAyB,OAAO,EAAE,CAAC;AAAA,UAC/D;AACA;AAAA,QACF;AAGA,YAAI,OAAO,IAAI,QAAQ,MAAM;AAC3B,gBAAM,KAAK,GAAG;AACd,kBAAQ,UAAU,GAAG;AAAA,QACvB;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,yBAAyB,KAAK;AAC5C,UAAI,QAAQ,SAAS;AACnB,gBAAQ,QAAQ,KAAc;AAAA,MAChC;AACA;AAAA,IACF;AAAA,EACF;AAEA,MAAI,QAAQ,QAAQ;AAClB,YAAQ,OAAO;AAAA,EACjB;AACF;;;AFnDA,IAAO,gBAAQ;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/parseLine.ts
|
|
2
|
+
var lineData = { data: null };
|
|
3
|
+
var dataBuffer = [];
|
|
4
|
+
function parseLine(line) {
|
|
5
|
+
if (line === "") {
|
|
6
|
+
if (dataBuffer.length === 0) return null;
|
|
7
|
+
const raw = dataBuffer.join("\n");
|
|
8
|
+
let data = raw;
|
|
9
|
+
const first = raw[0];
|
|
10
|
+
if (first === "{" || first === "[") {
|
|
11
|
+
try {
|
|
12
|
+
data = JSON.parse(raw);
|
|
13
|
+
} catch {
|
|
14
|
+
data = raw;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const result = {
|
|
18
|
+
id: lineData.id,
|
|
19
|
+
event: lineData.event,
|
|
20
|
+
data
|
|
21
|
+
};
|
|
22
|
+
lineData = { data: null };
|
|
23
|
+
dataBuffer.length = 0;
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
if (line.startsWith(":")) return null;
|
|
27
|
+
const colon = line.indexOf(":");
|
|
28
|
+
let field = "";
|
|
29
|
+
let value = "";
|
|
30
|
+
if (colon === -1) {
|
|
31
|
+
field = line;
|
|
32
|
+
} else {
|
|
33
|
+
field = line.slice(0, colon);
|
|
34
|
+
value = line.slice(colon + 1);
|
|
35
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
36
|
+
}
|
|
37
|
+
switch (field) {
|
|
38
|
+
case "id":
|
|
39
|
+
lineData.id = Number(value);
|
|
40
|
+
break;
|
|
41
|
+
case "event":
|
|
42
|
+
lineData.event = value;
|
|
43
|
+
break;
|
|
44
|
+
case "data":
|
|
45
|
+
dataBuffer.push(value);
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/stream.ts
|
|
54
|
+
var queue = [];
|
|
55
|
+
async function parseSSEStream({
|
|
56
|
+
renderStream,
|
|
57
|
+
options
|
|
58
|
+
}) {
|
|
59
|
+
const decoder = new TextDecoder();
|
|
60
|
+
while (true) {
|
|
61
|
+
try {
|
|
62
|
+
const { value, done } = await renderStream.read();
|
|
63
|
+
if (done) break;
|
|
64
|
+
const text = decoder.decode(value);
|
|
65
|
+
const lines = text.split("\n");
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
const lineVal = line.trimEnd();
|
|
68
|
+
let msg;
|
|
69
|
+
try {
|
|
70
|
+
msg = parseLine(lineVal);
|
|
71
|
+
if (msg && msg.data === "[DONE]") {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (options.onError) {
|
|
76
|
+
options.onError(new Error(`Failed to parse line: ${lineVal}`));
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (msg && msg.data != null) {
|
|
81
|
+
queue.push(msg);
|
|
82
|
+
options.onMessage(msg);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error("Error reading stream:", error);
|
|
87
|
+
if (options.onError) {
|
|
88
|
+
options.onError(error);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (options.onDone) {
|
|
94
|
+
options.onDone();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/index.ts
|
|
99
|
+
var index_default = parseSSEStream;
|
|
100
|
+
export {
|
|
101
|
+
index_default as default
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/parseLine.ts","../src/stream.ts","../src/index.ts"],"sourcesContent":["// Define the structure for parsed Server-Sent Event (SSE) line data\nexport type LineData = {\n id?: number;\n event?: string;\n data: Record<string, any> | string | null;\n error?: string;\n};\n\n// Global state variables to maintain parsing context across multiple lines\nlet lineData: LineData = { data: null }; // Stores event metadata (id, event type)\nlet dataBuffer: string[] = []; // Accumulates multi-line data segments\n\n/**\n * Parses a single line of Server-Sent Events (SSE) protocol\n *\n * Handles SSE fields: data, event, id, and comments.\n * Returns complete event data when an empty line is encountered,\n * otherwise updates internal state and returns null.\n */\nexport function parseLine(line: string): LineData | null {\n // Empty line indicates the end of an event - process and return accumulated data\n if (line === \"\") {\n if (dataBuffer.length === 0) return null;\n\n const raw = dataBuffer.join(\"\\n\");\n\n // Initialize data as raw string, attempt JSON parsing if it looks like JSON\n let data: any = raw;\n const first = raw[0];\n if (first === \"{\" || first === \"[\") {\n try {\n data = JSON.parse(raw);\n } catch {\n data = raw;\n }\n }\n\n // Create result with accumulated event data\n const result: LineData = {\n id: lineData.id,\n event: lineData.event,\n data,\n };\n\n // Reset state for next event\n lineData = { data: null };\n dataBuffer.length = 0;\n return result;\n }\n\n // Skip comment lines (start with ':')\n if (line.startsWith(\":\")) return null;\n\n // Parse field-value pairs (format: \"field: value\")\n const colon = line.indexOf(\":\");\n let field = \"\";\n let value = \"\";\n\n if (colon === -1) {\n field = line;\n } else {\n field = line.slice(0, colon);\n value = line.slice(colon + 1);\n if (value.startsWith(\" \")) value = value.slice(1);\n }\n\n // Update internal state based on field type\n switch (field) {\n case \"id\":\n lineData.id = Number(value);\n break;\n case \"event\":\n lineData.event = value;\n break;\n case \"data\":\n // Multi-line data segments are appended to buffer\n dataBuffer.push(value);\n break;\n default:\n // Ignore unknown fields\n break;\n }\n\n return null;\n}\n","import type { LineData, SSEProps } from \"./types\";\nimport { parseLine } from \"./parseLine\";\n\n// Queue to store parsed line data\nconst queue = [] as LineData[];\nexport async function parseSSEStream<T = any>({\n renderStream,\n options,\n}: SSEProps) {\n const decoder = new TextDecoder();\n while (true) {\n try {\n const { value, done } = await renderStream.read();\n if (done) break;\n\n // Decode and split text into lines\n const text = decoder.decode(value);\n const lines = text.split(\"\\n\");\n\n for (const line of lines) {\n const lineVal = line.trimEnd();\n\n let msg;\n try {\n msg = parseLine(lineVal);\n // Check for DONE flag and terminate if found\n if (msg && msg.data === \"[DONE]\") {\n return;\n }\n } catch (error) {\n if (options.onError) {\n options.onError(new Error(`Failed to parse line: ${lineVal}`));\n }\n continue;\n }\n\n // Process non-null messages\n if (msg && msg.data != null) {\n queue.push(msg);\n options.onMessage(msg);\n }\n }\n } catch (error) {\n console.error(\"Error reading stream:\", error);\n if (options.onError) {\n options.onError(error as Error);\n }\n break;\n }\n }\n // Call completion handler if provided\n if (options.onDone) {\n options.onDone();\n }\n}\n","export type { SSEMessage, LineData } from \"./types\";\nimport { parseSSEStream } from \"./stream\";\n\nexport default parseSSEStream;\n"],"mappings":";AASA,IAAI,WAAqB,EAAE,MAAM,KAAK;AACtC,IAAI,aAAuB,CAAC;AASrB,SAAS,UAAU,MAA+B;AAEvD,MAAI,SAAS,IAAI;AACf,QAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,UAAM,MAAM,WAAW,KAAK,IAAI;AAGhC,QAAI,OAAY;AAChB,UAAM,QAAQ,IAAI,CAAC;AACnB,QAAI,UAAU,OAAO,UAAU,KAAK;AAClC,UAAI;AACF,eAAO,KAAK,MAAM,GAAG;AAAA,MACvB,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAGA,UAAM,SAAmB;AAAA,MACvB,IAAI,SAAS;AAAA,MACb,OAAO,SAAS;AAAA,MAChB;AAAA,IACF;AAGA,eAAW,EAAE,MAAM,KAAK;AACxB,eAAW,SAAS;AACpB,WAAO;AAAA,EACT;AAGA,MAAI,KAAK,WAAW,GAAG,EAAG,QAAO;AAGjC,QAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,MAAI,QAAQ;AACZ,MAAI,QAAQ;AAEZ,MAAI,UAAU,IAAI;AAChB,YAAQ;AAAA,EACV,OAAO;AACL,YAAQ,KAAK,MAAM,GAAG,KAAK;AAC3B,YAAQ,KAAK,MAAM,QAAQ,CAAC;AAC5B,QAAI,MAAM,WAAW,GAAG,EAAG,SAAQ,MAAM,MAAM,CAAC;AAAA,EAClD;AAGA,UAAQ,OAAO;AAAA,IACb,KAAK;AACH,eAAS,KAAK,OAAO,KAAK;AAC1B;AAAA,IACF,KAAK;AACH,eAAS,QAAQ;AACjB;AAAA,IACF,KAAK;AAEH,iBAAW,KAAK,KAAK;AACrB;AAAA,IACF;AAEE;AAAA,EACJ;AAEA,SAAO;AACT;;;AChFA,IAAM,QAAQ,CAAC;AACf,eAAsB,eAAwB;AAAA,EAC5C;AAAA,EACA;AACF,GAAa;AACX,QAAM,UAAU,IAAI,YAAY;AAChC,SAAO,MAAM;AACX,QAAI;AACF,YAAM,EAAE,OAAO,KAAK,IAAI,MAAM,aAAa,KAAK;AAChD,UAAI,KAAM;AAGV,YAAM,OAAO,QAAQ,OAAO,KAAK;AACjC,YAAM,QAAQ,KAAK,MAAM,IAAI;AAE7B,iBAAW,QAAQ,OAAO;AACxB,cAAM,UAAU,KAAK,QAAQ;AAE7B,YAAI;AACJ,YAAI;AACF,gBAAM,UAAU,OAAO;AAEvB,cAAI,OAAO,IAAI,SAAS,UAAU;AAChC;AAAA,UACF;AAAA,QACF,SAAS,OAAO;AACd,cAAI,QAAQ,SAAS;AACnB,oBAAQ,QAAQ,IAAI,MAAM,yBAAyB,OAAO,EAAE,CAAC;AAAA,UAC/D;AACA;AAAA,QACF;AAGA,YAAI,OAAO,IAAI,QAAQ,MAAM;AAC3B,gBAAM,KAAK,GAAG;AACd,kBAAQ,UAAU,GAAG;AAAA,QACvB;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,yBAAyB,KAAK;AAC5C,UAAI,QAAQ,SAAS;AACnB,gBAAQ,QAAQ,KAAc;AAAA,MAChC;AACA;AAAA,IACF;AAAA,EACF;AAEA,MAAI,QAAQ,QAAQ;AAClB,YAAQ,OAAO;AAAA,EACjB;AACF;;;ACnDA,IAAO,gBAAQ;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sse-line-parser",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A simple parser for Server-Sent Events (SSE) streams",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"default": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/**/*"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"dev": "tsup --watch",
|
|
26
|
+
"clean": "rm -rf dist",
|
|
27
|
+
"typecheck": "tsc --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"sse",
|
|
31
|
+
"stream",
|
|
32
|
+
"parser",
|
|
33
|
+
"event-stream"
|
|
34
|
+
],
|
|
35
|
+
"author": "private_person",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.11.20",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
40
|
+
"@typescript-eslint/parser": "^6.21.0",
|
|
41
|
+
"eslint": "^8.56.0",
|
|
42
|
+
"tsup": "^8.0.2",
|
|
43
|
+
"typescript": "^5.3.3"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
}
|
|
48
|
+
}
|