From de566944332be8af3d083e5fa9c3efd60b54ed16 Mon Sep 17 00:00:00 2001 From: mptyl Date: Sat, 27 Jun 2026 20:49:00 +0200 Subject: [PATCH] feat(backend): LF-only JSONL line splitter Implement attachJsonlReader to split streams on \n only, handling trailing \r, and reassembling lines split across chunk boundaries. Co-Authored-By: Claude Opus 4.8 --- backend/src/rpc/line-splitter.ts | 17 +++++++++++++++++ backend/test/line-splitter.test.ts | 12 ++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 backend/src/rpc/line-splitter.ts create mode 100644 backend/test/line-splitter.test.ts diff --git a/backend/src/rpc/line-splitter.ts b/backend/src/rpc/line-splitter.ts new file mode 100644 index 00000000..99ecc06e --- /dev/null +++ b/backend/src/rpc/line-splitter.ts @@ -0,0 +1,17 @@ +import { StringDecoder } from "node:string_decoder"; +import type { Readable } from "node:stream"; + +export function attachJsonlReader(stream: Readable, onLine: (line: string) => void): () => void { + const decoder = new StringDecoder("utf8"); + let buffer = ""; + const emit = (line: string) => onLine(line.endsWith("\r") ? line.slice(0, -1) : line); + const onData = (chunk: Buffer | string) => { + buffer += typeof chunk === "string" ? chunk : decoder.write(chunk); + for (let nl; (nl = buffer.indexOf("\n")) !== -1; ) { + emit(buffer.slice(0, nl)); buffer = buffer.slice(nl + 1); + } + }; + const onEnd = () => { buffer += decoder.end(); if (buffer) { emit(buffer); buffer = ""; } }; + stream.on("data", onData); stream.on("end", onEnd); + return () => { stream.off("data", onData); stream.off("end", onEnd); }; +} diff --git a/backend/test/line-splitter.test.ts b/backend/test/line-splitter.test.ts new file mode 100644 index 00000000..9e5334e7 --- /dev/null +++ b/backend/test/line-splitter.test.ts @@ -0,0 +1,12 @@ +import { test, expect } from "vitest"; +import { Readable } from "node:stream"; +import { attachJsonlReader } from "../src/rpc/line-splitter.js"; + +test("riassembla righe spezzate tra chunk, split solo su \\n", async () => { + const lines: string[] = []; + const s = new Readable({ read() {} }); + attachJsonlReader(s, (l) => lines.push(l)); + s.push('{"a":1}\n{"b":'); s.push('2}\r\n{"u":" in stringa"}\n'); s.push(null); + await new Promise((r) => s.on("end", r)); + expect(lines).toEqual(['{"a":1}', '{"b":2}', '{"u":" in stringa"}']); +});