@@ -14,17 +14,49 @@ export class NdjsonRecordTooLargeError extends Error {
1414 */
1515 public readonly maxRecordBytes : number ;
1616
17- public constructor ( maxRecordBytes : number ) {
17+ /**
18+ * Valid records completed by the decoder before it rejected the oversized record.
19+ */
20+ public readonly decodedRecords : readonly unknown [ ] ;
21+
22+ public constructor ( maxRecordBytes : number , decodedRecords : readonly unknown [ ] = [ ] ) {
1823 super ( `The NDJSON record exceeds the maximum size of ${ maxRecordBytes } bytes.` ) ;
1924 this . name = 'NdjsonRecordTooLargeError' ;
2025 this . maxRecordBytes = maxRecordBytes ;
26+ this . decodedRecords = [ ...decodedRecords ] ;
2127
2228 // Restore the prototype chain, which is broken when subclassing a built-in
2329 // and compiling to CommonJS.
2430 Object . setPrototypeOf ( this , NdjsonRecordTooLargeError . prototype ) ;
2531 }
2632}
2733
34+ /**
35+ * Thrown when a completed NDJSON record is not valid JSON.
36+ *
37+ * @beta
38+ */
39+ export class NdjsonInvalidRecordError extends Error {
40+ /**
41+ * Valid records completed before the malformed record.
42+ */
43+ public readonly decodedRecords : readonly unknown [ ] ;
44+
45+ /**
46+ * The JSON parser error that caused this failure.
47+ */
48+ public readonly cause : Error ;
49+
50+ public constructor ( decodedRecords : readonly unknown [ ] , cause : Error ) {
51+ super ( 'The NDJSON record is not valid JSON.' ) ;
52+ this . name = 'NdjsonInvalidRecordError' ;
53+ this . decodedRecords = [ ...decodedRecords ] ;
54+ this . cause = cause ;
55+
56+ Object . setPrototypeOf ( this , NdjsonInvalidRecordError . prototype ) ;
57+ }
58+ }
59+
2860/**
2961 * Options controlling NDJSON record size enforcement.
3062 *
@@ -67,6 +99,9 @@ export function encodeNdjsonRecord(value: unknown, options?: INdjsonOptions): st
6799 * Call {@link NdjsonDecoder.decode} for each received chunk to obtain the
68100 * records completed by that chunk, then call {@link NdjsonDecoder.flush} once
69101 * the stream ends to obtain any trailing record that was not newline-terminated.
102+ * If a later record fails, its error exposes earlier valid records through
103+ * `decodedRecords`; the rejected completed line is consumed and any subsequent
104+ * buffered lines remain available to a later call.
70105 *
71106 * @beta
72107 */
@@ -83,7 +118,8 @@ export class NdjsonDecoder {
83118 * Appends a chunk and returns any records it completed.
84119 *
85120 * @param chunk - a fragment of the NDJSON stream
86- * @throws NdjsonRecordTooLargeError if a record exceeds the limit
121+ * @throws {@link NdjsonRecordTooLargeError } if a record exceeds the limit
122+ * @throws {@link NdjsonInvalidRecordError } if a completed record is malformed
87123 */
88124 public decode ( chunk : string ) : unknown [ ] {
89125 this . _buffer += chunk ;
@@ -99,7 +135,7 @@ export class NdjsonDecoder {
99135
100136 // A partial line that already exceeds the limit can never become a valid record.
101137 if ( Buffer . byteLength ( this . _buffer , 'utf8' ) > this . _maxRecordBytes ) {
102- throw new NdjsonRecordTooLargeError ( this . _maxRecordBytes ) ;
138+ throw new NdjsonRecordTooLargeError ( this . _maxRecordBytes , records ) ;
103139 }
104140
105141 return records ;
@@ -108,7 +144,8 @@ export class NdjsonDecoder {
108144 /**
109145 * Returns any trailing record that was not newline-terminated and resets the buffer.
110146 *
111- * @throws NdjsonRecordTooLargeError if the trailing record exceeds the limit
147+ * @throws {@link NdjsonRecordTooLargeError } if the trailing record exceeds the limit
148+ * @throws {@link NdjsonInvalidRecordError } if the trailing record is malformed
112149 */
113150 public flush ( ) : unknown [ ] {
114151 const records : unknown [ ] = [ ] ;
@@ -122,12 +159,18 @@ export class NdjsonDecoder {
122159
123160 private _processLine ( line : string , records : unknown [ ] ) : void {
124161 if ( Buffer . byteLength ( line , 'utf8' ) > this . _maxRecordBytes ) {
125- throw new NdjsonRecordTooLargeError ( this . _maxRecordBytes ) ;
162+ throw new NdjsonRecordTooLargeError ( this . _maxRecordBytes , records ) ;
126163 }
127164 const trimmed : string = line . trim ( ) ;
128165 if ( trimmed . length === 0 ) {
129166 return ;
130167 }
131- records . push ( JSON . parse ( trimmed ) ) ;
168+ try {
169+ records . push ( JSON . parse ( trimmed ) ) ;
170+ } catch ( error ) {
171+ const cause : Error =
172+ error instanceof Error ? error : new Error ( 'An unknown JSON parsing failure occurred.' ) ;
173+ throw new NdjsonInvalidRecordError ( records , cause ) ;
174+ }
132175 }
133176}
0 commit comments