mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
The Coach chat route had three identical `new ReadableStream({
start(controller) { try { ... } finally { controller.close(); } } })`
blocks (refusal / provider-error / happy path), differing only in
which frames they enqueued. Extract the boilerplate into
`src/lib/sse/create-stream.ts` so the v1.5 iOS streaming endpoints
inherit the same try/finally close semantics without copy-paste.
The helper accepts sync or async emit callbacks; the controller is
always closed in `finally` so a thrown emit never hangs the response.
Three unit tests pin the sync, async, and throw paths.
Closes Sr-H2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
49 lines
1.7 KiB
TypeScript
49 lines
1.7 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { createSseStream } from "../create-stream";
|
|
|
|
/**
|
|
* v1.4.22 W5 reconcile (Sr-H2) — extracted SSE-stream constructor.
|
|
*
|
|
* Pin the contract: emit can be sync or async, the controller is
|
|
* always closed (success or thrown), and the resulting stream reads
|
|
* as a real `ReadableStream<Uint8Array>`.
|
|
*/
|
|
async function readAll(stream: ReadableStream<Uint8Array>): Promise<string> {
|
|
const reader = stream.getReader();
|
|
const decoder = new TextDecoder();
|
|
let out = "";
|
|
for (;;) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
out += decoder.decode(value, { stream: true });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
describe("createSseStream", () => {
|
|
it("enqueues frames from a synchronous emit callback and closes", async () => {
|
|
const stream = createSseStream((controller) => {
|
|
controller.enqueue(new TextEncoder().encode("data: one\n\n"));
|
|
controller.enqueue(new TextEncoder().encode("data: two\n\n"));
|
|
});
|
|
expect(await readAll(stream)).toBe("data: one\n\ndata: two\n\n");
|
|
});
|
|
|
|
it("awaits an async emit callback before closing", async () => {
|
|
const stream = createSseStream(async (controller) => {
|
|
await Promise.resolve();
|
|
controller.enqueue(new TextEncoder().encode("data: async\n\n"));
|
|
});
|
|
expect(await readAll(stream)).toBe("data: async\n\n");
|
|
});
|
|
|
|
it("closes the stream cleanly even when emit throws", async () => {
|
|
const stream = createSseStream(() => {
|
|
throw new Error("emit-failure");
|
|
});
|
|
// Should not hang — the finally block always closes the
|
|
// controller. The reader sees an empty body.
|
|
expect(await readAll(stream)).toBe("");
|
|
});
|
|
});
|