Skip to content

Commit 1a66390

Browse files
committed
Fixed #216.
1 parent 5d41ab3 commit 1a66390

23 files changed

Lines changed: 208 additions & 26 deletions

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ stream-json/
117117
- Options: `packKeys`, `packStrings`, `packNumbers`, `streamKeys`, `streamStrings`, `streamNumbers`, `jsonStreaming`.
118118
- **Assembler** (`src/assembler.js`, implementation in `src/core/assembler.js`) interprets the token stream and reconstructs JavaScript objects. Plain class — no `EventEmitter` inheritance in 3.x.
119119
- Used internally by all streamers via `streamBase`.
120+
- Reads only packed tokens (`keyValue`, `stringValue`, `numberValue`); streamed chunks are ignored.
120121
- `Assembler.connectTo(stream, {onDone: asm => …})` is substrate-aware: accepts either a Node `Readable` (attaches `'data'` listener) or a Web `ReadableStream` (pumps via `getReader()`). Detection via `typeof stream.getReader === 'function'`. `asm.onDone(fn)` can set/clear the callback after construction.
121122
- For hot paths, prefer a manual `for await (const tok of readable) asm.consume(tok)` loop over `connectTo` — no async-closure overhead, errors propagate directly. `FlexAssembler` has the same shape.
122123
- `asm.tapChain` is a function for use in `chain()`.

ARCHITECTURE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ All filters are built on `filterBase` (`src/filters/filter-base.js`):
150150
- It maintains a path stack tracking the current JSON position.
151151
- `filter` option: a string, RegExp, or function `(stack, chunk) → boolean` that determines whether to accept or reject each subobject.
152152
- `makeStackDiffer` generates structural tokens (start/end object/array, key tokens) to reconstruct the surrounding JSON envelope when filtering.
153+
- Keys are tracked only from `keyValue` tokens, so key-based paths and parent recreation need packed keys from upstream (the parser's default). Replayed parent keys are always packed; their streamed form mirrors upstream unless `streamKeys` is set.
153154

154155
| Filter | specialAction | defaultAction | Effect |
155156
| --------- | ----------------------- | -------------- | ----------------------------------- |
@@ -163,7 +164,7 @@ All filters are built on `filterBase` (`src/filters/filter-base.js`):
163164
All streamers are built on `streamBase` (`src/streamers/stream-base.js`):
164165

165166
- `streamBase({push, first, level})` returns a factory that accepts `options` and returns a function for use in `chain()`.
166-
- Uses `Assembler` internally to reconstruct objects.
167+
- Uses `Assembler` internally to reconstruct objects; reads only packed tokens (`keyValue`, `stringValue`, `numberValue`), the parser's default.
167168
- `level` controls when to emit: level 0 for `streamValues`, level 1 for `streamArray`/`streamObject`.
168169
- `objectFilter` option enables early rejection: if `objectFilter(asm)` returns `false`, the object is abandoned without completing assembly.
169170
- `first` callback validates the opening token (e.g., `streamArray` requires `startArray`).

llms-full.txt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ parserStream.on('data', token => console.log(token.name));
196196

197197
## Assembler
198198

199-
`Assembler` — a plain class (no `EventEmitter` inheritance) that interprets the token stream and reconstructs JavaScript objects. 3.0 dropped the `'done'` event in favor of an `onDone` callback option.
199+
`Assembler` — a plain class (no `EventEmitter` inheritance) that interprets the token stream and reconstructs JavaScript objects. It reads only packed tokens (`keyValue`, `stringValue`, `numberValue`); streamed chunks are ignored, so the parser must pack keys, strings, and numbers (its default). 3.0 dropped the `'done'` event in favor of an `onDone` callback option.
200200

201201
Constructor options:
202202
- `reviver` (function) — like `JSON.parse` reviver. Called as `reviver(key, value)`.
@@ -399,6 +399,9 @@ All filters are built on `filterBase` and accept these common options:
399399
- **RegExp** — matches when `regExp.test(stack.join(separator))`.
400400
- **function** `(stack, chunk) => boolean` — custom matching logic.
401401
- `pathSeparator` (string, default: `'.'`) — separator for path matching.
402+
- `streamKeys` (boolean; seeded by `streamValues`) — replay parent keys as `startKey`/`stringChunk`/`endKey` too. Default: mirrors upstream (on once streamed keys were received). Replayed keys are always emitted as `keyValue`.
403+
- `packKeys` — deprecated no-op on filters (still configures the parser in a `withParser()` bag).
404+
- Input requirement: key-based paths and parent recreation need packed keys (`keyValue`) from upstream — the parser's default.
402405
- `once` (boolean) — if true, stop filtering after the first match.
403406
- `maxDepth` (number, default: `1024`) — maximum JSON nesting depth to evaluate; a token nested deeper throws a `RangeError`. A guard for untrusted input with unbounded nesting. Pass `Infinity` to disable.
404407
- `streamKeys` (boolean) — control key streaming in output.
@@ -407,7 +410,7 @@ Each filter ships in both substrates. The Node entry (`stream-json/filters/<name
407410

408411
### pick(options)
409412

410-
Passes only matching subobjects, discards everything else.
413+
Passes only matching subobjects, discards everything else. Key-based paths need packed keys (`keyValue`) from upstream — the parser's default.
411414

412415
```js
413416
import {pick} from 'stream-json/filters/pick.js';
@@ -424,7 +427,7 @@ const pipeline = pick.withParser({filter: 'data'});
424427

425428
### replace(options)
426429

427-
Replaces matching subobjects with a replacement value.
430+
Replaces matching subobjects with a replacement value. Needs packed keys (`keyValue`) from upstream — the parser's default; replayed parent keys are always packed.
428431

429432
Extra option:
430433
- `replacement` — the replacement:
@@ -450,7 +453,7 @@ chain([parser(), replace({
450453

451454
### ignore(options)
452455

453-
Removes matching subobjects completely. A variant of Replace with `replacement = none`.
456+
Removes matching subobjects completely. A variant of Replace with `replacement = none`. Needs packed keys (`keyValue`) from upstream — the parser's default; replayed parent keys are always packed.
454457

455458
```js
456459
import {ignore} from 'stream-json/filters/ignore.js';
@@ -462,7 +465,7 @@ chain([parser(), ignore({filter: /^\d+\.extra\b/}), stringer()]);
462465

463466
### filter(options)
464467

465-
Keeps matching subobjects while preserving the surrounding JSON structure.
468+
Keeps matching subobjects while preserving the surrounding JSON structure. Needs packed keys (`keyValue`) from upstream — the parser's default; replayed parent keys are always packed.
466469

467470
Extra option:
468471
- `acceptObjects` (boolean) — if true, accepts entire objects (not just tokens).
@@ -507,11 +510,11 @@ const differ = makeStackDiffer(/* previousStack */ []);
507510
// return differ(stack, chunk, options);
508511
```
509512

510-
The differ honors `streamKeys`, `streamValues`, `packKeys`, and `pathSeparator` from the filter's options.
513+
The differ honors `streamKeys`, `streamValues`, and `pathSeparator` from the filter's options. It always replays `keyValue`; the streamed form follows `streamKeys`, which by default mirrors upstream.
511514

512515
## Streamers
513516

514-
All streamers are built on `streamBase` and produce `{key, value}` objects. Each is generic in the assembled value type — `streamArray<T>()`, `streamValues<T>()`, `streamObject<T>()` (and their `.withParser<T>()`) carry `T` through to the item's `value` field; the default is `unknown`. The item shape is the exported `KeyedValue<K, T>` type (`stream-json/streamers/stream-base.js`): `K` is `string` for `streamObject` (the property name) and `number` for `streamArray` (the array index) and `streamValues` (a sequential counter); `StreamArrayItem<T>` / `StreamObjectItem<T>` / `StreamValuesItem<T>` are aliases of it.
517+
All streamers are built on `streamBase` and produce `{key, value}` objects. They read only packed tokens (`keyValue`, `stringValue`, `numberValue`) — the parser's default. Each is generic in the assembled value type — `streamArray<T>()`, `streamValues<T>()`, `streamObject<T>()` (and their `.withParser<T>()`) carry `T` through to the item's `value` field; the default is `unknown`. The item shape is the exported `KeyedValue<K, T>` type (`stream-json/streamers/stream-base.js`): `K` is `string` for `streamObject` (the property name) and `number` for `streamArray` (the array index) and `streamValues` (a sequential counter); `StreamArrayItem<T>` / `StreamObjectItem<T>` / `StreamValuesItem<T>` are aliases of it.
515518

516519
Common option:
517520
- `objectFilter` (function) `(asm) => boolean|null` — called during assembly. Return `true` to accept, `false` to reject (abandon assembly), `null`/`undefined` for undecided.
@@ -626,7 +629,7 @@ Creates a `gen(parser(options), fn(options))` pipeline — a function for use in
626629

627630
Browser-safe Web-only entry: `stream-json/web/utils/with-parser.js` (has only `asWebStream`).
628631

629-
Most components export `.withParser(options)`, `.withParserAsStream(options)`, and `.withParserAsWebStream(options)` static methods as a convenience:
632+
Most components export `.withParser(options)`, `.withParserAsStream(options)`, and `.withParserAsWebStream(options)` static methods as a convenience. They prepend `packKeys: true` to the options bag, so the parser packs keys even under `packValues: false` (filters and streamers need them); the bare utility adds nothing:
630633

631634
```js
632635
// These are equivalent:
@@ -683,7 +686,7 @@ fs.createReadStream('data.json').pipe(v);
683686

684687
### FlexAssembler
685688

686-
Like Assembler but with custom containers (Map, Set, custom classes) at specific paths. Standalone clone — same API surface (`connectTo`, `tapChain`, `onDone`). `FlexAssembler.connectTo` is substrate-aware: accepts either a Node `Readable` or a Web `ReadableStream`.
689+
Like Assembler but with custom containers (Map, Set, custom classes) at specific paths. Reads only packed tokens, like Assembler. Standalone clone — same API surface (`connectTo`, `tapChain`, `onDone`). `FlexAssembler.connectTo` is substrate-aware: accepts either a Node `Readable` or a Web `ReadableStream`.
687690

688691
Options:
689692
- `objectRules` — array of rules for objects: `{filter, create, add, finalize?}`.

llms.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ For the SAX-style event API on Web, use the `EventTarget`-based variants from `s
6767

6868
### Assembler
6969

70-
`Assembler` — class that reconstructs JS objects from tokens. Receives a per-value callback via the `onDone` option.
70+
`Assembler` — class that reconstructs JS objects from tokens. Reads only packed tokens (`keyValue`, `stringValue`, `numberValue`); streamed chunks are ignored. Receives a per-value callback via the `onDone` option.
7171

7272
```js
7373
import Assembler from 'stream-json/assembler.js';
@@ -118,7 +118,7 @@ e.addEventListener('keyValue', ev => console.log(ev.detail));
118118

119119
## Filters
120120

121-
All filters accept `{filter, pathSeparator, once, streamKeys, maxDepth}` options. `filter` can be a string, RegExp, or `(stack, chunk) => boolean`. `maxDepth` caps the JSON nesting depth a filter evaluates (default `1024`; a deeper token throws a `RangeError`, `Infinity` disables the limit).
121+
All filters accept `{filter, pathSeparator, once, maxDepth, streamKeys}` options. Key-based paths and parent recreation need packed keys from upstream (the parser's default). Replayed parent keys are always packed; their streamed form mirrors upstream unless `streamKeys` says otherwise. `packKeys` on a filter is a deprecated no-op. `filter` can be a string, RegExp, or `(stack, chunk) => boolean`. `maxDepth` caps the JSON nesting depth a filter evaluates (default `1024`; a deeper token throws a `RangeError`, `Infinity` disables the limit).
122122

123123
- **`pick(options)`** — passes only matching subobjects, discards the rest.
124124
- **`replace(options)`** — replaces matching subobjects. Extra option: `replacement` (function, token, or array of tokens).
@@ -143,7 +143,7 @@ chain([
143143

144144
## Streamers
145145

146-
Assemble complete JS objects from a token stream. All produce `{key, value}` objects, generic in the assembled value type (`streamArray<T>()`, `streamValues<T>()`, `streamObject<T>()`; `value` defaults to `unknown`). The item shape is the exported `KeyedValue<K, T>` type — `key` is a `string` for `streamObject`, a `number` for the others.
146+
Assemble complete JS objects from a token stream. They read only packed tokens (`keyValue`, `stringValue`, `numberValue`) — the parser's default. All produce `{key, value}` objects, generic in the assembled value type (`streamArray<T>()`, `streamValues<T>()`, `streamObject<T>()`; `value` defaults to `unknown`). The item shape is the exported `KeyedValue<K, T>` type — `key` is a `string` for `streamObject`, a `number` for the others.
147147

148148
- **`streamValues(options)`** — streams successive JSON values. Use with `jsonStreaming` or after `pick`.
149149
- **`streamArray(options)`** — streams elements of a single top-level array.

src/core/assembler.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export interface AssemblerOptions<T = unknown> {
3333
* `onDone` option (or call `.onDone(fn)`) to receive a callback each time a
3434
* top-level value is fully assembled.
3535
*
36+
* Reads only packed tokens (`keyValue`, `stringValue`, `numberValue`); streamed
37+
* chunks are ignored, so the upstream parser must pack keys, strings, and numbers
38+
* (its default).
39+
*
3640
* Generic in `T` (default `unknown`) — the type of the fully assembled value.
3741
* Declare `new Assembler<MyShape>()` to type `current` and `tapChain()`. Read
3842
* `current` in the `onDone` callback (when it actually holds the completed `T`);

src/core/filters/filter-base.d.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,19 @@ declare namespace filterBase {
3434
* Pass `Infinity` to disable the limit.
3535
*/
3636
maxDepth?: number;
37-
/** Initial value for `streamKeys`. Controls streaming of replayed keys. */
37+
/** Initial value for `streamKeys`. */
3838
streamValues?: boolean;
39-
/** Emit streaming key tokens (`startKey`/`stringChunk`/`endKey`) when replaying delayed keys. */
39+
/**
40+
* Replay parent keys as `startKey`/`stringChunk`/`endKey` too. Default: mirrors
41+
* upstream — on once streamed keys have been received. Replayed keys are always
42+
* emitted as `keyValue`.
43+
*/
4044
streamKeys?: boolean;
41-
/** Expect packed `keyValue` tokens from upstream. */
45+
/**
46+
* @deprecated No effect on filters: keys are tracked only when they arrive as
47+
* `keyValue`, and replayed keys are always packed. In a `withParser()` options
48+
* bag it still configures the parser. To be removed in the next major.
49+
*/
4250
packKeys?: boolean;
4351
}
4452

src/core/filters/filter-base.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ const filterBase =
5656
maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
5757
/** @type {(stack: any[], chunk?: any) => boolean} */
5858
let filter = defaultFilter;
59-
let streamKeys = true;
59+
let streamKeys = false,
60+
mirrorStreamKeys = true;
6061
if (options) {
6162
if (typeof options.filter == 'function') {
6263
filter = options.filter;
@@ -65,8 +66,14 @@ const filterBase =
6566
} else if (options.filter instanceof RegExp) {
6667
filter = regExpFilter(options.filter, separator);
6768
}
68-
if ('streamValues' in options) streamKeys = options.streamValues;
69-
if ('streamKeys' in options) streamKeys = options.streamKeys;
69+
if ('streamValues' in options) {
70+
streamKeys = options.streamValues;
71+
mirrorStreamKeys = false;
72+
}
73+
if ('streamKeys' in options) {
74+
streamKeys = options.streamKeys;
75+
mirrorStreamKeys = false;
76+
}
7077
}
7178
const sanitizedOptions = {...options, filter, streamKeys, separator};
7279
let state = 'check',
@@ -204,6 +211,7 @@ const filterBase =
204211
switch (action) {
205212
case 'process-key':
206213
if (chunk.name === 'startKey') {
214+
if (mirrorStreamKeys) sanitizedOptions.streamKeys = true; // replay keys in the forms received
207215
state = 'process-key';
208216
continue recheck;
209217
}
@@ -282,9 +290,7 @@ const makeStackDiffer =
282290
if (options?.streamKeys) {
283291
returnTokens.push({name: 'startKey'}, {name: 'stringChunk', value: key}, {name: 'endKey'});
284292
}
285-
if (options?.packKeys || !options?.streamKeys) {
286-
returnTokens.push({name: 'keyValue', value: key});
287-
}
293+
returnTokens.push({name: 'keyValue', value: key});
288294
} else if (typeof key == 'number' && options?.skippedArrayValue) {
289295
for (let i = Math.max(0, previousStack[commonLength] + 1); i < key; ++i) {
290296
returnTokens.push(...options.skippedArrayValue);
@@ -318,9 +324,7 @@ const makeStackDiffer =
318324
if (options?.streamKeys) {
319325
returnTokens.push({name: 'startKey'}, {name: 'stringChunk', value: key}, {name: 'endKey'});
320326
}
321-
if (options?.packKeys || !options?.streamKeys) {
322-
returnTokens.push({name: 'keyValue', value: key});
323-
}
327+
returnTokens.push({name: 'keyValue', value: key});
324328
}
325329
}
326330

src/core/filters/filter.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import filterBase from './filter-base.js';
44

55
/**
66
* Filters subobjects from a token stream while preserving the original JSON shape.
7+
* Needs packed keys (`keyValue`) from upstream to track paths and recreate parents — the
8+
* parser's default; replayed parent keys are always packed.
79
*
810
* This is the pure, stream-agnostic factory — no `.asStream` / `.asWebStream` adapters
911
* attached. For the Node-flavored entry (with both adapters) import from

src/core/filters/ignore.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import filterBase from './filter-base.js';
44

55
/**
66
* Removes matching subobjects from a token stream entirely.
7+
* Needs packed keys (`keyValue`) from upstream to track paths and recreate parents — the
8+
* parser's default; replayed parent keys are always packed.
79
*
810
* This is the pure, stream-agnostic factory — no `.asStream` / `.asWebStream` adapters
911
* attached. For the Node-flavored entry (with both adapters) import from

src/core/filters/pick.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import filterBase from './filter-base.js';
44

55
/**
66
* Picks matching subobjects from a token stream, ignoring the rest.
7+
* Key-based paths need packed keys (`keyValue`) from upstream — the parser's default.
78
*
89
* This is the pure, stream-agnostic factory — no `.asStream` / `.asWebStream` adapters
910
* attached. For the Node-flavored entry (with both adapters) import from

0 commit comments

Comments
 (0)