/** Serialize one LF-delimited JSON record. */
export function serializeJsonLine(value) {
    return `${JSON.stringify(value)}\n`;
}
/**
 * Attach a strict LF-only, UTF-8 byte-bounded JSONL reader.
 *
 * The stream is paused before queued chunks are drained, so one turn cannot parse
 * an unbounded number of frames. Oversized records are discarded through their LF;
 * the next bounded record remains readable.
 */
export function attachJsonlLineReader(stream, onLine, options = {}) {
    const maxFrameBytes = options.maxFrameBytes ?? options.maxLineChars ?? Number.POSITIVE_INFINITY;
    const maxBytesPerTurn = options.maxBytesPerTurn ?? Number.POSITIVE_INFINITY;
    const maxLinesPerTurn = options.maxLinesPerTurn ?? Number.POSITIVE_INFINITY;
    const chunks = [];
    let frameParts = [];
    let frameBytes = 0;
    let discarding = false;
    let scheduled;
    let ended = false;
    let detached = false;
    const finishFrame = () => {
        if (discarding) {
            discarding = false;
            frameParts = [];
            frameBytes = 0;
            return;
        }
        const frame = Buffer.concat(frameParts, frameBytes);
        frameParts = [];
        frameBytes = 0;
        const withoutCr = frame.at(-1) === 0x0d ? frame.subarray(0, -1) : frame;
        onLine(withoutCr.toString("utf8"));
    };
    const consume = (chunk, byteBudget, lineBudget) => {
        let offset = 0;
        let lines = 0;
        while (offset < chunk.length && offset < byteBudget && lines < lineBudget) {
            const budgetEnd = Math.min(chunk.length, offset + (byteBudget - offset));
            const newline = chunk.indexOf(0x0a, offset);
            const end = newline !== -1 && newline < budgetEnd ? newline : budgetEnd;
            const part = chunk.subarray(offset, end);
            if (!discarding && frameBytes + part.length <= maxFrameBytes) {
                if (part.length > 0)
                    frameParts.push(part);
                frameBytes += part.length;
            }
            else if (!discarding) {
                discarding = true;
                frameParts = [];
                frameBytes = 0;
                options.onOversizedLine?.();
            }
            offset = end;
            if (newline === end) {
                offset += 1;
                finishFrame();
                lines += 1;
            }
            if (end === budgetEnd && newline !== end)
                break;
        }
        return { offset, lines };
    };
    const schedule = () => {
        if (!scheduled && !detached)
            scheduled = setImmediate(drain);
    };
    const drain = () => {
        scheduled = undefined;
        let bytes = 0;
        let lines = 0;
        while (chunks.length > 0 && bytes < maxBytesPerTurn && lines < maxLinesPerTurn) {
            const chunk = chunks[0];
            const result = consume(chunk, maxBytesPerTurn - bytes, maxLinesPerTurn - lines);
            bytes += result.offset;
            lines += result.lines;
            if (result.offset === chunk.length)
                chunks.shift();
            else {
                chunks[0] = chunk.subarray(result.offset);
                break;
            }
        }
        if (chunks.length > 0)
            schedule();
        else if (ended) {
            if (!discarding && frameBytes > 0)
                finishFrame();
            frameParts = [];
            frameBytes = 0;
        }
        else
            stream.resume();
    };
    const onData = (chunk) => {
        stream.pause();
        chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
        schedule();
    };
    const onEnd = () => {
        ended = true;
        if (scheduled) {
            clearImmediate(scheduled);
            scheduled = undefined;
            drain();
        }
        else if (chunks.length === 0 && !discarding && frameBytes > 0)
            finishFrame();
    };
    stream.on("data", onData);
    stream.on("end", onEnd);
    return () => {
        detached = true;
        if (scheduled)
            clearImmediate(scheduled);
        stream.off("data", onData);
        stream.off("end", onEnd);
        chunks.length = 0;
    };
}
//# sourceMappingURL=jsonl.js.map