Skip to content

Commit ed5f9cd

Browse files
committed
Add returnValue and currentRootValue properties to the state object passed to onChunk and onRootValue callbacks of parseChunked
1 parent 9f6dfa6 commit ed5f9cd

5 files changed

Lines changed: 79 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## next
2+
3+
- `parseChunked()`:
4+
- Added `returnValue` and `currentRootValue` properties to the state object passed to `onChunk` and `onRootValue` callbacks
5+
16
## 1.0.0 (2026-03-09)
27

38
- `parseChunked()`:

README.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,22 @@ Functions like [`JSON.parse()`](https://developer.mozilla.org/en-US/docs/Web/Jav
4646

4747
```ts
4848
function parseChunked(input: Iterable<Chunk> | AsyncIterable<Chunk>, reviver?: Reviver): Promise<any>;
49-
function parseChunked(input: Iterable<Chunk> | AsyncIterable<Chunk>, options?: ParseChunkedOptions): Promise<any>;
49+
function parseChunked(input: Iterable<Chunk> | AsyncIterable<Chunk>, options?: ParseOptions): Promise<any>;
5050
function parseChunked(input: () => (Iterable<Chunk> | AsyncIterable<Chunk>), reviver?: Reviver): Promise<any>;
51-
function parseChunked(input: () => (Iterable<Chunk> | AsyncIterable<Chunk>), options?: ParseChunkedOptions): Promise<any>;
51+
function parseChunked(input: () => (Iterable<Chunk> | AsyncIterable<Chunk>), options?: ParseOptions): Promise<any>;
5252

5353
type Chunk = string | Buffer | Uint8Array;
5454
type Reviver = (this: any, key: string, value: any) => any;
55-
type ParseChunkedOptions = {
55+
type ParseOptions = {
5656
reviver?: Reviver;
5757
mode?: 'json' | 'jsonl' | 'auto';
58-
onRootValue?: (value: any, state: ParseChunkState) => void;
59-
onChunk?: (chunkParsed: number, chunk: string | null, pending: string | null, state: ParseChunkState) => void;
58+
onRootValue?: (value: any, state: ParseChunkedState) => void;
59+
onChunk?: (chunkParsed: number, chunk: string | null, pending: string | null, state: ParseChunkedState) => void;
6060
};
61-
type ParseChunkState = {
61+
type ParseChunkedState = {
6262
mode: 'json' | 'jsonl';
63+
returnValue: any;
64+
currentRootValue: any;
6365
rootValuesCount: number;
6466
consumed: number;
6567
parsed: number;
@@ -88,7 +90,15 @@ You can pass `reviver` either as the second argument (`parseChunked(input, reviv
8890

8991
`options.onRootValue` is called when a root value is parsed and finalized. When `onRootValue` is specified, `parseChunked()` resolves to the number of processed root values (instead of returning parsed value(s)), which allows processing huge or infinite streams without accumulating all values in memory.
9092

91-
`options.onChunk` is called after each input chunk is processed and once at the end with `chunk = null`. It provides parsing progress and parser state (`consumed`, `parsed`, current mode and root values count).
93+
`options.onChunk` is called after each input chunk is processed and once at the end with `chunk = null`. It provides parsing progress and parser state as chunks are processed.
94+
95+
The `state` object passed to `onRootValue` and `onChunk` callbacks has the following properties:
96+
- `consumed` – number of characters consumed so far
97+
- `parsed` – number of characters parsed so far (not necessarily the same when a chunk ends in the middle of a token)
98+
- `mode` – current parsing mode (`json` or `jsonl`)
99+
- `rootValuesCount` – number of root values parsed so far
100+
- `currentRootValue` – current root value being parsed
101+
- `returnValue` – current return value state, i.e. what `parseChunked()` will return when finished (either the parsed value or the number of root values, depending on whether `onRootValue` is specified)
92102

93103
Examples:
94104

index.d.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ declare module '@discoveryjs/json-ext' {
22
type Chunk = string | Uint8Array | Buffer;
33

44
type Reviver = (this: any, key: string, value: any) => any;
5-
type ParseChunkState = {
5+
type ParseChunkedState = {
66
readonly mode: 'json' | 'jsonl';
7+
readonly returnValue: any;
8+
readonly currentRootValue: any;
79
readonly rootValuesCount: number;
810
readonly consumed: number;
911
readonly parsed: number;
1012
};
11-
type OnRootValue = (value: any, state: ParseChunkState) => void;
12-
type OnChunk = (chunkParsed: number, chunk: string | null, pending: string | null, state: ParseChunkState) => void;
13+
type OnRootValue = (value: any, state: ParseChunkedState) => void;
14+
type OnChunk = (chunkParsed: number, chunk: string | null, pending: string | null, state: ParseChunkedState) => void;
1315
type ParseOptions = {
1416
reviver?: Reviver;
1517
mode?: 'json' | 'jsonl' | 'auto';

src/parse-chunked.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,18 @@ function createChunkParser(parseMode, reviver, onRootValue, onChunk) {
132132
get mode() {
133133
return parseMode === MODE_JSONL ? 'jsonl' : 'json';
134134
},
135+
get returnValue() {
136+
return typeof onRootValue === 'function'
137+
? rootValuesCount
138+
: rootValues !== null
139+
? rootValues
140+
: currentRootValue !== NO_VALUE
141+
? currentRootValue
142+
: undefined;
143+
},
144+
get currentRootValue() {
145+
return currentRootValue !== NO_VALUE ? currentRootValue : undefined;
146+
},
135147
get rootValuesCount() {
136148
return rootValuesCount;
137149
},
@@ -547,10 +559,11 @@ function createChunkParser(parseMode, reviver, onRootValue, onChunk) {
547559
onChunk(0, null, null, state);
548560
}
549561

550-
if (typeof onRootValue === 'function') {
551-
return rootValuesCount;
552-
}
562+
const result = state.returnValue;
563+
564+
rootValues = null;
565+
currentRootValue = NO_VALUE;
553566

554-
return rootValues !== null ? rootValues : currentRootValue;
567+
return result;
555568
}
556569
}

src/parse-chunked.test.js

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ function parse(chunks, options) {
88
return parseChunked(() => chunks, options);
99
}
1010

11+
function clone(value) {
12+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
13+
}
14+
1115
function split(str, chunkLen = 1) {
1216
const chunks = [];
1317

@@ -579,72 +583,81 @@ describe('parseChunked()', () => {
579583
it('should report chunk progress when onChunk is used', async () => {
580584
const chunks = [];
581585
const actual = await parse(['{"a":', '1,', ' "b"', ':2,', '"c":3 }\n'], {
582-
onChunk(chunkParsed, chunk, pending, { consumed, parsed }) {
586+
onChunk(chunkParsed, chunk, pending, { mode, consumed, parsed, returnValue, currentRootValue }) {
583587
chunks.push({
588+
mode,
584589
chunkParsed,
585590
chunk,
586591
pending,
587592
consumed,
588-
parsed
593+
parsed,
594+
returnValue: clone(returnValue), // clone to avoid mutation in later chunks
595+
currentRootValue: clone(currentRootValue) // clone to avoid mutation in later chunks
589596
});
590597
}
591598
});
592599
assert.deepStrictEqual(actual, { a: 1, b: 2, c: 3 });
593600
assert.deepStrictEqual(chunks, [
594-
{ chunkParsed: 1, chunk: '{"a":', pending: '"a":', consumed: 5, parsed: 1 },
595-
{ chunkParsed: 5, chunk: '1,', pending: ',', consumed: 7, parsed: 6 },
596-
{ chunkParsed: 0, chunk: ' "b"', pending: ', "b"', consumed: 11, parsed: 6 },
597-
{ chunkParsed: 7, chunk: ':2,', pending: ',', consumed: 14, parsed: 13 },
598-
{ chunkParsed: 9, chunk: '"c":3 }\n', pending: null, consumed: 22, parsed: 22 },
599-
{ chunkParsed: 0, chunk: null, pending: null, consumed: 22, parsed: 22 }
601+
{ mode: 'json', chunkParsed: 1, chunk: '{"a":', pending: '"a":', consumed: 5, parsed: 1, returnValue: {}, currentRootValue: {} },
602+
{ mode: 'json', chunkParsed: 5, chunk: '1,', pending: ',', consumed: 7, parsed: 6, returnValue: { a: 1 }, currentRootValue: { a: 1 } },
603+
{ mode: 'json', chunkParsed: 0, chunk: ' "b"', pending: ', "b"', consumed: 11, parsed: 6, returnValue: { a: 1 }, currentRootValue: { a: 1 } },
604+
{ mode: 'json', chunkParsed: 7, chunk: ':2,', pending: ',', consumed: 14, parsed: 13, returnValue: { a: 1, b: 2 }, currentRootValue: { a: 1, b: 2 } },
605+
{ mode: 'json', chunkParsed: 9, chunk: '"c":3 }\n', pending: null, consumed: 22, parsed: 22, returnValue: { a: 1, b: 2, c: 3 }, currentRootValue: { a: 1, b: 2, c: 3 } },
606+
{ mode: 'json', chunkParsed: 0, chunk: null, pending: null, consumed: 22, parsed: 22, returnValue: { a: 1, b: 2, c: 3 }, currentRootValue: { a: 1, b: 2, c: 3 } }
600607
]);
601608
});
602609

603610
it('jsonl', async () => {
604611
const chunks = [];
605612
const actual = await parse(['{"a":1', '}\n', '2\n[', '3]'], {
606613
mode: 'jsonl',
607-
onChunk(chunkParsed, chunk, pending, { consumed, parsed }) {
614+
onChunk(chunkParsed, chunk, pending, { mode, consumed, parsed, returnValue, currentRootValue }) {
608615
chunks.push({
616+
mode,
609617
chunkParsed,
610618
chunk,
611619
pending,
612620
consumed,
613-
parsed
621+
parsed,
622+
returnValue: clone(returnValue), // clone to avoid mutation in later chunks
623+
currentRootValue: clone(currentRootValue) // clone to avoid mutation in later chunks
614624
});
615625
}
616626
});
617627
assert.deepStrictEqual(actual, [{ a: 1 }, 2, [3]]);
618628
assert.deepStrictEqual(chunks, [
619-
{ chunkParsed: 1, chunk: '{"a":1', pending: '"a":1', consumed: 6, parsed: 1 },
620-
{ chunkParsed: 7, chunk: '}\n', pending: null, consumed: 8, parsed: 8 },
621-
{ chunkParsed: 3, chunk: '2\n[', pending: null, consumed: 11, parsed: 11 },
622-
{ chunkParsed: 2, chunk: '3]', pending: null, consumed: 13, parsed: 13 },
623-
{ chunkParsed: 0, chunk: null, pending: null, consumed: 13, parsed: 13 }
629+
{ mode: 'jsonl', chunkParsed: 1, chunk: '{"a":1', pending: '"a":1', consumed: 6, parsed: 1, returnValue: [], currentRootValue: {} },
630+
{ mode: 'jsonl', chunkParsed: 7, chunk: '}\n', pending: null, consumed: 8, parsed: 8, returnValue: [{ a: 1 }], currentRootValue: { a: 1 } },
631+
{ mode: 'jsonl', chunkParsed: 3, chunk: '2\n[', pending: null, consumed: 11, parsed: 11, returnValue: [{ a: 1 }, 2], currentRootValue: [] },
632+
{ mode: 'jsonl', chunkParsed: 2, chunk: '3]', pending: null, consumed: 13, parsed: 13, returnValue: [{ a: 1 }, 2, [3]], currentRootValue: [3] },
633+
{ mode: 'jsonl', chunkParsed: 0, chunk: null, pending: null, consumed: 13, parsed: 13, returnValue: [{ a: 1 }, 2, [3]], currentRootValue: [3] }
624634
]);
625635
});
626636

627637
it('auto', async () => {
628638
const chunks = [];
629639
const actual = await parse(['{"a":1', '}\n', '2\n[', '3]'], {
630640
mode: 'auto',
631-
onChunk(chunkParsed, chunk, pending, { consumed, parsed }) {
641+
onChunk(chunkParsed, chunk, pending, { mode, consumed, parsed, returnValue, currentRootValue }) {
632642
chunks.push({
643+
mode,
633644
chunkParsed,
634645
chunk,
635646
pending,
636647
consumed,
637-
parsed
648+
parsed,
649+
returnValue: clone(returnValue), // clone to avoid mutation in later chunks
650+
currentRootValue: clone(currentRootValue) // clone to avoid mutation in later chunks
638651
});
639652
}
640653
});
641654
assert.deepStrictEqual(actual, [{ a: 1 }, 2, [3]]);
642655
assert.deepStrictEqual(chunks, [
643-
{ chunkParsed: 1, chunk: '{"a":1', pending: '"a":1', consumed: 6, parsed: 1 },
644-
{ chunkParsed: 7, chunk: '}\n', pending: null, consumed: 8, parsed: 8 },
645-
{ chunkParsed: 3, chunk: '2\n[', pending: null, consumed: 11, parsed: 11 },
646-
{ chunkParsed: 2, chunk: '3]', pending: null, consumed: 13, parsed: 13 },
647-
{ chunkParsed: 0, chunk: null, pending: null, consumed: 13, parsed: 13 }
656+
{ mode: 'json', chunkParsed: 1, chunk: '{"a":1', pending: '"a":1', consumed: 6, parsed: 1, returnValue: {}, currentRootValue: {} },
657+
{ mode: 'json', chunkParsed: 7, chunk: '}\n', pending: null, consumed: 8, parsed: 8, returnValue: { a: 1 }, currentRootValue: { a: 1 } },
658+
{ mode: 'jsonl', chunkParsed: 3, chunk: '2\n[', pending: null, consumed: 11, parsed: 11, returnValue: [{ a: 1 }, 2], currentRootValue: [] },
659+
{ mode: 'jsonl', chunkParsed: 2, chunk: '3]', pending: null, consumed: 13, parsed: 13, returnValue: [{ a: 1 }, 2, [3]], currentRootValue: [3] },
660+
{ mode: 'jsonl', chunkParsed: 0, chunk: null, pending: null, consumed: 13, parsed: 13, returnValue: [{ a: 1 }, 2, [3]], currentRootValue: [3] }
648661
]);
649662
});
650663
});

0 commit comments

Comments
 (0)