assistant-stream 0.3.9 → 0.3.10

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.
@@ -1 +1 @@
1
- {"version":3,"file":"LineDecoderStream.d.ts","sourceRoot":"","sources":["../../../../src/core/utils/stream/LineDecoderStream.ts"],"names":[],"mappings":"AAAA,qBAAa,iBAAkB,SAAQ,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC;IACpE,OAAO,CAAC,MAAM,CAAM;;CA2BrB"}
1
+ {"version":3,"file":"LineDecoderStream.d.ts","sourceRoot":"","sources":["../../../../src/core/utils/stream/LineDecoderStream.ts"],"names":[],"mappings":"AAAA,qBAAa,iBAAkB,SAAQ,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC;IACpE,OAAO,CAAC,MAAM,CAAM;;CA6BrB"}
@@ -7,7 +7,9 @@ export class LineDecoderStream extends TransformStream {
7
7
  const lines = this.buffer.split("\n");
8
8
  // Process all complete lines
9
9
  for (let i = 0; i < lines.length - 1; i++) {
10
- controller.enqueue(lines[i]);
10
+ // Strip trailing \r to handle \r\n (CRLF) line endings per the SSE spec.
11
+ const line = lines[i];
12
+ controller.enqueue(line.endsWith("\r") ? line.slice(0, -1) : line);
11
13
  }
12
14
  // Keep the last incomplete line in the buffer
13
15
  this.buffer = lines[lines.length - 1] || "";
@@ -1 +1 @@
1
- {"version":3,"file":"LineDecoderStream.js","sourceRoot":"","sources":["../../../../src/core/utils/stream/LineDecoderStream.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,iBAAkB,SAAQ,eAA+B;IAC5D,MAAM,GAAG,EAAE,CAAC;IAEpB;QACE,KAAK,CAAC;YACJ,SAAS,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;gBAC/B,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC;gBACrB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAEtC,6BAA6B;gBAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;oBAC1C,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC/B,CAAC;gBAED,8CAA8C;gBAC9C,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;YAC9C,CAAC;YACD,KAAK,EAAE,GAAG,EAAE;gBACV,kEAAkE;gBAClE,iEAAiE;gBACjE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;oBAChB,MAAM,IAAI,KAAK,CACb,0CAA0C,IAAI,CAAC,MAAM,GAAG,CACzD,CAAC;gBACJ,CAAC;YACH,CAAC;SACF,CAAC,CAAC;IACL,CAAC;CACF"}
1
+ {"version":3,"file":"LineDecoderStream.js","sourceRoot":"","sources":["../../../../src/core/utils/stream/LineDecoderStream.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,iBAAkB,SAAQ,eAA+B;IAC5D,MAAM,GAAG,EAAE,CAAC;IAEpB;QACE,KAAK,CAAC;YACJ,SAAS,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;gBAC/B,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC;gBACrB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAEtC,6BAA6B;gBAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;oBAC1C,yEAAyE;oBACzE,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;oBACvB,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBACrE,CAAC;gBAED,8CAA8C;gBAC9C,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;YAC9C,CAAC;YACD,KAAK,EAAE,GAAG,EAAE;gBACV,kEAAkE;gBAClE,iEAAiE;gBACjE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;oBAChB,MAAM,IAAI,KAAK,CACb,0CAA0C,IAAI,CAAC,MAAM,GAAG,CACzD,CAAC;gBACJ,CAAC;YACH,CAAC;SACF,CAAC,CAAC;IACL,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistant-stream",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Streaming utilities for AI assistants",
5
5
  "keywords": [
6
6
  "ai",
@@ -161,6 +161,31 @@ describe("AssistantTransportDecoder", () => {
161
161
  expect(decodedChunks).toEqual(originalChunks);
162
162
  });
163
163
 
164
+ it("should decode SSE with CRLF line endings", async () => {
165
+ // Simulate a server that uses \r\n line endings (common in HTTP)
166
+ const sseText =
167
+ 'data: {"type":"text-delta","textDelta":"Hello","path":[]}\r\n\r\n' +
168
+ "data: [DONE]\r\n\r\n";
169
+
170
+ const encoder = new TextEncoder();
171
+ const stream = new ReadableStream<Uint8Array>({
172
+ start(controller) {
173
+ controller.enqueue(encoder.encode(sseText));
174
+ controller.close();
175
+ },
176
+ });
177
+
178
+ const decodedStream = stream.pipeThrough(new AssistantTransportDecoder());
179
+ const decodedChunks = await collectChunks(decodedStream);
180
+
181
+ expect(decodedChunks).toHaveLength(1);
182
+ expect(decodedChunks[0]).toEqual({
183
+ type: "text-delta",
184
+ textDelta: "Hello",
185
+ path: [],
186
+ });
187
+ });
188
+
164
189
  it("should throw error when stream ends without [DONE]", async () => {
165
190
  // Manually create an SSE stream without [DONE]
166
191
  const sseText =
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { LineDecoderStream } from "./LineDecoderStream";
3
+
4
+ async function collectLines(stream: ReadableStream<string>): Promise<string[]> {
5
+ const reader = stream.getReader();
6
+ const lines: string[] = [];
7
+ while (true) {
8
+ const { done, value } = await reader.read();
9
+ if (done) break;
10
+ lines.push(value);
11
+ }
12
+ return lines;
13
+ }
14
+
15
+ function createTextStream(chunks: string[]): ReadableStream<string> {
16
+ return new ReadableStream<string>({
17
+ start(controller) {
18
+ for (const chunk of chunks) {
19
+ controller.enqueue(chunk);
20
+ }
21
+ controller.close();
22
+ },
23
+ });
24
+ }
25
+
26
+ describe("LineDecoderStream", () => {
27
+ it("should split lines on LF (\\n)", async () => {
28
+ const stream = createTextStream(["line1\nline2\nline3\n"]);
29
+ const lines = await collectLines(
30
+ stream.pipeThrough(new LineDecoderStream()),
31
+ );
32
+ expect(lines).toEqual(["line1", "line2", "line3"]);
33
+ });
34
+
35
+ it("should split lines on CRLF (\\r\\n)", async () => {
36
+ const stream = createTextStream(["line1\r\nline2\r\nline3\r\n"]);
37
+ const lines = await collectLines(
38
+ stream.pipeThrough(new LineDecoderStream()),
39
+ );
40
+ expect(lines).toEqual(["line1", "line2", "line3"]);
41
+ });
42
+
43
+ it("should handle CRLF split across chunks", async () => {
44
+ // The \r is at the end of one chunk and \n at the start of the next
45
+ const stream = createTextStream(["line1\r", "\nline2\r\n"]);
46
+ const lines = await collectLines(
47
+ stream.pipeThrough(new LineDecoderStream()),
48
+ );
49
+ expect(lines).toEqual(["line1", "line2"]);
50
+ });
51
+
52
+ it("should handle empty lines with LF", async () => {
53
+ const stream = createTextStream(["line1\n\nline2\n"]);
54
+ const lines = await collectLines(
55
+ stream.pipeThrough(new LineDecoderStream()),
56
+ );
57
+ expect(lines).toEqual(["line1", "", "line2"]);
58
+ });
59
+
60
+ it("should handle empty lines with CRLF", async () => {
61
+ const stream = createTextStream(["line1\r\n\r\nline2\r\n"]);
62
+ const lines = await collectLines(
63
+ stream.pipeThrough(new LineDecoderStream()),
64
+ );
65
+ expect(lines).toEqual(["line1", "", "line2"]);
66
+ });
67
+
68
+ it("should handle mixed LF and CRLF", async () => {
69
+ const stream = createTextStream(["line1\nline2\r\nline3\n"]);
70
+ const lines = await collectLines(
71
+ stream.pipeThrough(new LineDecoderStream()),
72
+ );
73
+ expect(lines).toEqual(["line1", "line2", "line3"]);
74
+ });
75
+
76
+ it("should handle multiple chunks", async () => {
77
+ const stream = createTextStream(["li", "ne1\nli", "ne2\n"]);
78
+ const lines = await collectLines(
79
+ stream.pipeThrough(new LineDecoderStream()),
80
+ );
81
+ expect(lines).toEqual(["line1", "line2"]);
82
+ });
83
+
84
+ it("should throw on incomplete line at end of stream", async () => {
85
+ const stream = createTextStream(["line1\nincomplete"]);
86
+ await expect(
87
+ collectLines(stream.pipeThrough(new LineDecoderStream())),
88
+ ).rejects.toThrow("Stream ended with an incomplete line");
89
+ });
90
+
91
+ it("should handle SSE-like data with CRLF", async () => {
92
+ const sseData = 'data: {"type":"text"}\r\n\r\ndata: [DONE]\r\n\r\n';
93
+ const stream = createTextStream([sseData]);
94
+ const lines = await collectLines(
95
+ stream.pipeThrough(new LineDecoderStream()),
96
+ );
97
+ expect(lines).toEqual(['data: {"type":"text"}', "", "data: [DONE]", ""]);
98
+ });
99
+ });
@@ -9,7 +9,9 @@ export class LineDecoderStream extends TransformStream<string, string> {
9
9
 
10
10
  // Process all complete lines
11
11
  for (let i = 0; i < lines.length - 1; i++) {
12
- controller.enqueue(lines[i]);
12
+ // Strip trailing \r to handle \r\n (CRLF) line endings per the SSE spec.
13
+ const line = lines[i]!;
14
+ controller.enqueue(line.endsWith("\r") ? line.slice(0, -1) : line);
13
15
  }
14
16
 
15
17
  // Keep the last incomplete line in the buffer