* Yields individual lines from a byte stream. * * Handles \r, \n, and \r\n line endings, including \r\n split across chunks. * Uses offset tracking within each decoded chunk to avoid O(n²) buffer growth. * Strips BOM (U+FEFF) from the very first line. * Throws if the internal line buffer exceed
(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>)
| 81 | * Throws if the internal line buffer exceeds 10MB. |
| 82 | */ |
| 83 | async function* iterLines(stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): AsyncGenerator<string> { |
| 84 | const decoder = new TextDecoder('utf-8'); |
| 85 | const buffer: string[] = []; |
| 86 | let bufferSize = 0; |
| 87 | let trailingCR = false; |
| 88 | let isFirstLine = true; |
| 89 | |
| 90 | for await (const chunk of toAsyncIterable(stream)) { |
| 91 | const text = decoder.decode(chunk, { stream: true }); |
| 92 | let offset = 0; |
| 93 | |
| 94 | // Handle \r\n split across chunks: if the previous chunk ended with \r |
| 95 | // and this one starts with \n, skip the \n (it's the second half of \r\n) |
| 96 | if (trailingCR) { |
| 97 | trailingCR = false; |
| 98 | if (text.length > 0 && text[0] === '\n') { |
| 99 | offset = 1; |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | while (offset < text.length) { |
| 104 | const crIdx = text.indexOf('\r', offset); |
| 105 | const lfIdx = text.indexOf('\n', offset); |
| 106 | |
| 107 | // No more line endings in this chunk — buffer the rest |
| 108 | if (crIdx === -1 && lfIdx === -1) { |
| 109 | const remaining = text.slice(offset); |
| 110 | buffer.push(remaining); |
| 111 | bufferSize += remaining.length; |
| 112 | if (bufferSize > MAX_LINE_BUFFER_SIZE) { |
| 113 | throw new Error('SSE line buffer exceeded 10MB'); |
| 114 | } |
| 115 | break; |
| 116 | } |
| 117 | |
| 118 | let endIdx: number; |
| 119 | let skipLen: number; |
| 120 | |
| 121 | if (crIdx !== -1 && (lfIdx === -1 || crIdx < lfIdx)) { |
| 122 | // \r found before \n (or no \n at all) |
| 123 | endIdx = crIdx; |
| 124 | if (crIdx + 1 < text.length) { |
| 125 | // Peek ahead: \r\n or bare \r |
| 126 | skipLen = text[crIdx + 1] === '\n' ? 2 : 1; |
| 127 | } else { |
| 128 | // \r at end of chunk — might be \r\n split across chunks |
| 129 | trailingCR = true; |
| 130 | skipLen = 1; |
| 131 | } |
| 132 | } else { |
| 133 | // \n found before \r (or no \r at all) |
| 134 | // Safe: at least one of crIdx/lfIdx is != -1, and we're in the else |
| 135 | // branch, so lfIdx must be != -1 |
| 136 | endIdx = lfIdx; |
| 137 | skipLen = 1; |
| 138 | } |
| 139 | |
| 140 | const segment = text.slice(offset, endIdx); |
no test coverage detected