UNPKG

assistant-stream

Version:

Streaming utilities for AI assistants

184 lines (169 loc) • 5.43 kB
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"); }); });