assistant-stream
Version:
Streaming utilities for AI assistants
184 lines (169 loc) • 5.43 kB
text/typescript
import { describe, expect, it, vi } from "vitest";
import { createSSEJsonDecoder, createSSEJsonEncoder } from "./SSEJson";
import { promiseWithResolvers } from "../../../utils/promiseWithResolvers";
async function collectChunks<T>(stream: ReadableStream<T>): Promise<T[]> {
const reader = stream.getReader();
const chunks: T[] = [];
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
} finally {
reader.releaseLock();
}
return chunks;
}
function fromValues<T>(values: T[]): ReadableStream<T> {
return new ReadableStream<T>({
start(controller) {
for (const value of values) controller.enqueue(value);
controller.close();
},
});
}
function fromSSEText(text: string): ReadableStream<Uint8Array<ArrayBuffer>> {
return fromValues([new TextEncoder().encode(text)]) as ReadableStream<
Uint8Array<ArrayBuffer>
>;
}
const jsonParse = (
data: string,
controller: TransformStreamDefaultController<unknown>,
) => {
controller.enqueue(JSON.parse(data));
};
describe("createSSEJsonEncoder", () => {
it("frames each chunk as an sse data line", async () => {
const encoded = await collectChunks(
createSSEJsonEncoder()(fromValues([{ a: 1 }, { b: 2 }])),
);
const text = new TextDecoder().decode(
encoded.reduce(
(acc, part) => new Uint8Array([...acc, ...part]),
new Uint8Array(),
),
);
expect(text).toBe('data: {"a":1}\n\ndata: {"b":2}\n\n');
});
it("appends the done marker on flush when configured", async () => {
const encoded = await collectChunks(
createSSEJsonEncoder("[DONE]")(fromValues([{ a: 1 }])),
);
const text = new TextDecoder().decode(
encoded.reduce(
(acc, part) => new Uint8Array([...acc, ...part]),
new Uint8Array(),
),
);
expect(text).toBe('data: {"a":1}\n\ndata: [DONE]\n\n');
});
});
describe("createSSEJsonDecoder", () => {
it.each(["", "\uFEFF"])(
"decodes a stream with %j as its BOM prefix",
async (prefix) => {
const chunks = await collectChunks(
createSSEJsonDecoder({ parse: jsonParse })(
fromSSEText(`${prefix}data: {"a":1}\n\n`),
),
);
expect(chunks).toEqual([{ a: 1 }]);
},
);
it.each([0, 1, 2, 3, 4, 5, 6])(
"ignores a field prefixed by a second BOM when bytes split at %i",
async (split) => {
const bytes = new TextEncoder().encode(
'\uFEFF\uFEFFdata: {"ignored":true}\ndata: {"a":1}\n\n',
);
const chunks = await collectChunks(
createSSEJsonDecoder({ parse: jsonParse })(
fromValues([bytes.slice(0, split), bytes.slice(split)]),
),
);
expect(chunks).toEqual([{ a: 1 }]);
},
);
it("preserves a BOM inside a JSON string", async () => {
const chunks = await collectChunks(
createSSEJsonDecoder({ parse: jsonParse })(
fromSSEText('data: {"text":"\uFEFFkept"}\n\n'),
),
);
expect(chunks).toEqual([{ text: "\uFEFFkept" }]);
});
it("terminates and cancels an open input on the done marker", async () => {
const cancelled = promiseWithResolvers<void>();
const input = new ReadableStream<Uint8Array<ArrayBuffer>>({
start(controller) {
controller.enqueue(
new TextEncoder().encode(
'data: {"a":1}\n\ndata: [DONE]\n\ndata: {"b":2}\n\n',
),
);
},
cancel() {
cancelled.resolve();
},
});
const chunks = await collectChunks(
createSSEJsonDecoder({
parse: jsonParse,
done: { marker: "[DONE]" },
})(input),
);
expect(chunks).toEqual([{ a: 1 }]);
await cancelled.promise;
});
it("does not stop at a done marker in an unknown event", async () => {
const onUnknownEvent = vi.fn();
const chunks = await collectChunks(
createSSEJsonDecoder({
parse: jsonParse,
strict: false,
onUnknownEvent,
done: { marker: "[DONE]" },
})(
fromSSEText(
'event: custom\ndata: [DONE]\n\ndata: {"a":1}\n\ndata: [DONE]\n\n',
),
),
);
expect(chunks).toEqual([{ a: 1 }]);
expect(onUnknownEvent).toHaveBeenCalledExactlyOnceWith("custom");
});
it("invokes the missing callback when the stream ends without the marker", async () => {
const onMissing = vi.fn();
await collectChunks(
createSSEJsonDecoder({
parse: jsonParse,
done: { marker: "[DONE]", onMissing },
})(fromSSEText('data: {"a":1}\n\n')),
);
expect(onMissing).toHaveBeenCalledOnce();
});
it("throws on unknown event names in strict mode", async () => {
await expect(
collectChunks(
createSSEJsonDecoder({
parse: jsonParse,
strict: true,
})(fromSSEText('event: custom\ndata: {"a":1}\n\n')),
),
).rejects.toThrow("Unknown SSE event type: custom");
});
it("reports unknown event names through the lenient callback", async () => {
const onUnknownEvent = vi.fn();
const chunks = await collectChunks(
createSSEJsonDecoder({
parse: jsonParse,
strict: false,
onUnknownEvent,
})(fromSSEText('event: custom\ndata: {"a":1}\n\ndata: {"b":2}\n\n')),
);
expect(chunks).toEqual([{ b: 2 }]);
expect(onUnknownEvent).toHaveBeenCalledExactlyOnceWith("custom");
});
});