json-web-streams 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,16 +35,14 @@ import { JSONParseStream } from "json-web-streams";
35
35
 
36
36
  // data.json contains: [{ "x": 1 }, { "x": 2 }, { "x": 3 }]
37
37
  const response = await fetch("https://example.com/data.json");
38
- await response.body
38
+
39
+ const stream = response.body
39
40
  .pipeThrough(new TextDecoderStream())
40
- .pipeThrough(new JSONParseStream(["$[*]"]))
41
- .pipeTo(
42
- new WritableStream({
43
- write({ value }) {
44
- console.log(value);
45
- },
46
- }),
47
- );
41
+ .pipeThrough(new JSONParseStream(["$[*]"]));
42
+
43
+ for await (const { value } of stream) {
44
+ console.log(value);
45
+ }
48
46
 
49
47
  // Output:
50
48
  // {"x": 1}
@@ -52,18 +50,6 @@ await response.body
52
50
  // {"x": 3}
53
51
  ```
54
52
 
55
- > [!TIP]
56
- > If you don't have to support Safari, [most other environments](https://caniuse.com/mdn-api_readablestream_--asynciterator) let you use a nicer syntax for consuming stream output as an async iterator:
57
- >
58
- > ```ts
59
- > const stream = response.body
60
- > .pipeThrough(new TextDecoderStream())
61
- > .pipeThrough(new JSONParseStream(["$[*]"]));
62
- > for await (const { value } of stream) {
63
- > console.log(value);
64
- > }
65
- > ```
66
-
67
53
  ## API
68
54
 
69
55
  ```ts
@@ -158,68 +144,60 @@ If you only have one JSONPath query, you can ignore `key`. But if you have more
158
144
 
159
145
  <!-- prettier-ignore -->
160
146
  ```ts
161
- await new ReadableStream({
147
+ const stream = new ReadableStream({
162
148
  start(controller) {
163
149
  controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
164
150
  controller.close();
165
151
  },
166
152
  })
167
- .pipeThrough(new JSONParseStream(["$.bar[*]", "$.foo[*]"]))
168
- .pipeTo(
169
- new WritableStream({
170
- write(record) {
171
- if (record.key === "$.bar[*]") {
172
- // Do something with the values from bar
173
- } else {
174
- // Do something with the values from foo
175
- }
176
- },
177
- }),
178
- );
153
+ .pipeThrough(new JSONParseStream(["$.bar[*]", "$.foo[*]"]));
154
+
155
+ for await (const record of stream) {
156
+ if (record.key === "$.bar[*]") {
157
+ // Do something with the values from bar
158
+ } else {
159
+ // Do something with the values from foo
160
+ }
161
+ }
179
162
  ```
180
163
 
181
164
  Or with a manually defined key:
182
165
 
183
166
  <!-- prettier-ignore -->
184
167
  ```ts
185
- await new ReadableStream({
168
+ const stream = new ReadableStream({
186
169
  start(controller) {
187
170
  controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
188
171
  controller.close();
189
172
  },
190
173
  })
191
- .pipeThrough(new JSONParseStream([{ key: "bar", path: "$.bar[*]" }, "$.foo[*]"]))
192
- .pipeTo(
193
- new WritableStream({
194
- write(record) {
195
- if (record.key === "bar") {
196
- // Do something with the values from bar
197
- } else {
198
- // Do something with the values from foo
199
- }
200
- },
201
- }),
202
- );
174
+ .pipeThrough(new JSONParseStream([{ key: "bar", path: "$.bar[*]" }, "$.foo[*]"]));
175
+
176
+ for await (const record of stream) {
177
+ if (record.key === "bar") {
178
+ // Do something with the values from bar
179
+ } else {
180
+ // Do something with the values from foo
181
+ }
182
+ }
203
183
  ```
204
184
 
205
185
  `wildcardKeys` is defined when you have a wildcard in an object (not an array) somewhere in your JSONPath. For example:
206
186
 
207
187
  <!-- prettier-ignore -->
208
188
  ```ts
209
- await new ReadableStream({
189
+ const stream = new ReadableStream({
210
190
  start(controller) {
211
191
  controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
212
192
  controller.close();
213
193
  },
214
194
  })
215
- .pipeThrough(new JSONParseStream(["$[*]"]))
216
- .pipeTo(
217
- new WritableStream({
218
- write(record) {
219
- console.log(record);
220
- },
221
- }),
222
- );
195
+ .pipeThrough(new JSONParseStream(["$[*]"]));
196
+
197
+ for await (const record of stream) {
198
+ console.log(record);
199
+ }
200
+
223
201
  // Output:
224
202
  // { key: "$[*]", value: [1, 2], wildcardKeys: ["foo"] },
225
203
  // { key: "$[*]", value: ["a", "b", "c"], wildcardKeys: ["bar"] },
@@ -230,7 +208,7 @@ The purpose of `wildcardKeys` is to allow you to easily distinguish different ty
230
208
  > [!WARNING]
231
209
  > It is possible to have two JSONPath queries that output overlapping objects, like if your data is `{ "foo": [1, 2] }` and you query for both `$` and `$.foo`. This will emit two objects: `{ foo: [1, 2] }` and `[1, 2]`. Due to how json-web-streams works internally, both of those objects share the same array instance, meaning that if the array in one is mutated it will affect the other.
232
210
  >
233
- > Some schema validation libraries do a deep clone of objects they validate. In that case, you won't have this issue. Otherwise, in the rare case that you query for overlapping objects, you will have to handle this problem, such as by deep cloning one of the objects.
211
+ > Some schema validation libraries do a deep clone of objects they validate. In that case, you won't have this issue.
234
212
 
235
213
  #### Schema validation and types for `JSONParseStream` output
236
214
 
@@ -242,7 +220,7 @@ To use schema validation for a JSONPath query, then pass an object `{ path: JSON
242
220
  ```ts
243
221
  import * as z from "zod";
244
222
 
245
- await new ReadableStream({
223
+ const stream = new ReadableStream({
246
224
  start(controller) {
247
225
  controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
248
226
  controller.close();
@@ -253,29 +231,26 @@ await new ReadableStream({
253
231
  { path: "$.foo[*]", schema: z.number() },
254
232
  { path: "$.bar[*]", schema: z.string() },
255
233
  ]),
256
- )
257
- .pipeTo(
258
- new WritableStream({
259
- write(record) {
260
- if (record.key === "$.foo[*]") {
261
- // Type of record.value is number
262
- } else {
263
- // Type of record.value is string
264
- }
265
- },
266
- }),
267
234
  );
235
+
236
+ for await (const record of stream) {
237
+ if (record.key === "$.foo[*]") {
238
+ // Type of record.value is `number`
239
+ } else {
240
+ // Type of record.value is `string`
241
+ }
242
+ }
268
243
  ```
269
244
 
270
245
  For JSONPath queries with no schema, emitted values will have the `unknown` type.
271
246
 
272
- The type of the `key` property will be either the string literal `path` from the input paramter (such as `"$.foo[*]`) or whatever you put in the `key` property of the input. For example:
247
+ The type of the `key` property will be either the string literal `path` from the input paramter (such as `"$.foo[*]`) or whatever you put in the `key` property of the input, so you can use it to discriminate between object types in the output. For example:
273
248
 
274
249
  <!-- prettier-ignore -->
275
250
  ```ts
276
251
  import * as z from "zod";
277
252
 
278
- await new ReadableStream({
253
+ const stream = new ReadableStream({
279
254
  start(controller) {
280
255
  controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
281
256
  controller.close();
@@ -286,18 +261,15 @@ await new ReadableStream({
286
261
  { key: "foo", path: "$.foo[*]", schema: z.number() },
287
262
  { key: "bar", path: "$.bar[*]", schema: z.string() },
288
263
  ]),
289
- )
290
- .pipeTo(
291
- new WritableStream({
292
- write(record) {
293
- if (record.key === "foo") {
294
- // Type of record.value is number
295
- } else {
296
- // Type of record.value is string
297
- }
298
- },
299
- }),
300
264
  );
265
+
266
+ for await (const record of stream) {
267
+ if (record.key === "foo") {
268
+ // Type of record.value is `number`
269
+ } else {
270
+ // Type of record.value is `string`
271
+ }
272
+ }
301
273
  ```
302
274
 
303
275
  > [!TIP]
@@ -309,7 +281,7 @@ json-web-streams supports a subset of JSONPath. Currently the supported componen
309
281
 
310
282
  - **The root node**, represented by the symbol `$` which must be the first character of any JSONPath query.
311
283
 
312
- - **Name selectors** which are like accessing a property in a JS object. For instance if you have an object like `{ "foo": { bar: 5 } }`, then `$.foo.bar` refers to the value `5`. You can also write this in the more verbose bracket notation like `$["foo"]["bar"]` or `$["foo", "bar"]`, which is useful if your key names include characters that need escaping. You can also mix them like `$.foo["bar"]` or use single quotes like `$['foo']['bar']` - all of these JSONPath queries have the same meaning.
284
+ - **Name selectors** which are like accessing a property in a JS object. For instance if you have an object like `{ "foo": { bar: 5 } }`, then `$.foo.bar` refers to the value `5`. You can also write this in the more verbose bracket notation like `$["foo"]["bar"]`, which is useful if your key names include characters that need escaping. You can also mix them like `$.foo["bar"]` or use single quotes like `$['foo']['bar']` - all of these JSONPath queries have the same meaning.
313
285
 
314
286
  - **Wildcard selectors** which select every value in an array or object. With this JSON `{ "foo": { "a": 1, "b": 2, "c": 3 } }`, the JSONPath query `$.foo[*]` would emit the three individual numbers `1`, `2`, and `3`. If the inner object was changed to an array like `{ "foo": [1, 2, 3] }`, the same JSONPath query would emit the same values.
315
287
 
@@ -327,7 +299,7 @@ Or if the array is at the root if the object like this data:
327
299
  [{ "x": 1 }, { "x": 2 }, { "x": 3 }]
328
300
  ```
329
301
 
330
- then you'd write something like `$[*]` to emit each object (`{ x: 1}`, `{x: 2}`, `{x: 3}`), or `$[*].key` to emit just the numbers (`1`, `2`, `3`).
302
+ then you'd write something like `$[*]` to emit each object (`{ x: 1}`, `{x: 2}`, `{x: 3}`), or `$[*].x` to emit just the numbers (`1`, `2`, `3`).
331
303
 
332
304
  To emit the whole object at once (okay in that case you wouldn't use this library, but maybe just for testing, or for `multi` mode) you just use `$`.
333
305
 
@@ -352,7 +324,7 @@ You want to get all the values in `foo` and all the values in `bar`. You could d
352
324
 
353
325
  <!-- prettier-ignore -->
354
326
  ```ts
355
- await new ReadableStream({
327
+ const stream = new ReadableStream({
356
328
  start(controller) {
357
329
  controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
358
330
  controller.close();
@@ -360,25 +332,22 @@ await new ReadableStream({
360
332
  })
361
333
  .pipeThrough(
362
334
  new JSONParseStream(["$.foo[*]", "$.bar[*]"]),
363
- )
364
- .pipeTo(
365
- new WritableStream({
366
- write(record) {
367
- if (record.key === "$.foo[*]") {
368
- // 1, 2
369
- } else {
370
- // a, b, c
371
- }
372
- },
373
- }),
374
335
  );
336
+
337
+ for await (const record of stream) {
338
+ if (record.key === "$.foo[*]") {
339
+ // 1, 2
340
+ } else {
341
+ // a, b, c
342
+ }
343
+ }
375
344
  ```
376
345
 
377
346
  Or you could use one JSONPath query with a wildcard, and then use `.wildcardKeys` to distinguish the objects:
378
347
 
379
348
  <!-- prettier-ignore -->
380
349
  ```ts
381
- await new ReadableStream({
350
+ const stream = new ReadableStream({
382
351
  start(controller) {
383
352
  controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
384
353
  controller.close();
@@ -386,18 +355,15 @@ await new ReadableStream({
386
355
  })
387
356
  .pipeThrough(
388
357
  new JSONParseStream(["$[*][*]"]),
389
- )
390
- .pipeTo(
391
- new WritableStream({
392
- write(record) {
393
- if (record.wildcardKeys[0] === "foo") {
394
- // 1, 2
395
- } else {
396
- // a, b, c
397
- }
398
- },
399
- }),
400
358
  );
359
+
360
+ for await (const record of stream) {
361
+ if (record.wildcardKeys[0] === "foo") {
362
+ // 1, 2
363
+ } else {
364
+ // a, b, c
365
+ }
366
+ }
401
367
  ```
402
368
 
403
369
  Using multiple JSONPath queries is a little more explicit, but using wildcard keys is more concise, especially if you had more than just two types of objects. And instead of known keys like `foo` and `bar` your JSON had some unknown keys, then using a wildcard would be your only option.
@@ -410,7 +376,7 @@ In this example, Zod schemas enforce that `record.value` is either a `string` or
410
376
  ```ts
411
377
  import * as z from "zod";
412
378
 
413
- await new ReadableStream({
379
+ const stream = new ReadableStream({
414
380
  start(controller) {
415
381
  controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
416
382
  controller.close();
@@ -421,16 +387,13 @@ await new ReadableStream({
421
387
  { path: "$.foo[*]", schema: z.number() },
422
388
  { path: "$.bar[*]", schema: z.string() },
423
389
  ]),
424
- )
425
- .pipeTo(
426
- new WritableStream({
427
- write(record) {
428
- if (record.key === "$.foo[*]") {
429
- // 1, 2
430
- } else {
431
- // a, b, c
432
- }
433
- },
434
- }),
435
390
  );
391
+
392
+ for await (const record of stream) {
393
+ if (record.key === "$.foo[*]") {
394
+ // Type of record.value is `number` rather than `unknown`
395
+ } else {
396
+ // Type of record.value is `string` rather than `unknown`
397
+ }
398
+ }
436
399
  ```
@@ -3,19 +3,13 @@ import { jsonPathToPathArray } from "./jsonPathToPathArray.js";
3
3
 
4
4
  //#region src/JSONParseStream.ts
5
5
  const isEqual = (x, y) => {
6
- if (!y) return false;
7
- if (x.type === "wildcard") {
8
- if (y.mode === "ARRAY") return true;
9
- else if (y.mode === "OBJECT") return true;
10
- } else if (y.mode === "OBJECT" && x.value === y.key) return true;
11
- return false;
6
+ if (x.type === "wildcard") return y.mode === "ARRAY" || y.mode === "OBJECT";
7
+ return y.mode === "OBJECT" && x.value === y.key;
12
8
  };
13
9
  var JSONParseStream = class extends TransformStream {
14
10
  _parser;
15
11
  constructor(jsonPaths, options) {
16
12
  let parser;
17
- let minPathArrayLength = Infinity;
18
- let maxPathArrayLength = -Infinity;
19
13
  const jsonPathInfos = jsonPaths.map((row) => {
20
14
  let key;
21
15
  let path;
@@ -32,66 +26,42 @@ var JSONParseStream = class extends TransformStream {
32
26
  if (wildcardIndexes === void 0) wildcardIndexes = [];
33
27
  wildcardIndexes.push(i);
34
28
  }
35
- const matches = pathArray.length === 0 ? "yes" : "unknown";
36
- if (pathArray.length > maxPathArrayLength) maxPathArrayLength = pathArray.length;
37
- if (pathArray.length < minPathArrayLength) minPathArrayLength = pathArray.length;
38
29
  return {
39
30
  key,
40
- matches,
41
31
  path,
42
32
  pathArray,
43
33
  validate: schema?.["~standard"].validate,
44
34
  wildcardIndexes
45
35
  };
46
36
  });
47
- const jsonPathInfosThatMatch = new Set(jsonPathInfos.filter((info) => info.matches === "yes"));
48
- const updateMatches = (type) => {
49
- for (const info of jsonPathInfos) {
50
- const pathArray = info.pathArray;
51
- if ((info.matches === "unknown" && type === "push" || info.matches !== "unknown" && info.matches !== "noBeforeEnd" && type === "key") && parser.stack.length === pathArray.length) {
52
- let pathMatches = "yes";
53
- for (let j = 0; j < pathArray.length; j++) {
54
- let stackComponent = parser.stack[j + 1];
55
- if (!stackComponent) {
56
- if (type === "key") stackComponent = parser;
57
- else if (pathArray[j].type === "wildcard") {
58
- pathMatches = "yes";
59
- break;
60
- }
61
- }
62
- if (!isEqual(pathArray[j], stackComponent)) {
63
- if (j < pathArray.length - 1) pathMatches = "noBeforeEnd";
64
- else pathMatches = "noAtEnd";
65
- break;
66
- }
67
- }
68
- info.matches = pathMatches;
69
- if (pathMatches === "yes") jsonPathInfosThatMatch.add(info);
70
- else jsonPathInfosThatMatch.delete(info);
71
- }
72
- }
37
+ const makeLevel = (jsonPathInfos$1, depth, matchesAbove) => {
38
+ return {
39
+ jsonPathInfos: jsonPathInfos$1,
40
+ matchesHere: jsonPathInfos$1.some((info) => info.pathArray.length === depth),
41
+ matchesAbove,
42
+ matchesBelow: jsonPathInfos$1.some((info) => info.pathArray.length > depth)
43
+ };
44
+ };
45
+ const levels = [makeLevel(jsonPathInfos, 0, false)];
46
+ const updateLevel = (depth) => {
47
+ const parent = levels[depth - 1];
48
+ levels[depth] = makeLevel(parent.jsonPathInfos.filter((info) => info.pathArray.length >= depth && isEqual(info.pathArray[depth - 1], parser)), depth, parent.matchesAbove || parent.matchesHere);
73
49
  };
74
50
  super({
75
51
  start(controller) {
76
52
  parser = new JSONParseStreamRaw({
77
53
  multi: options?.multi,
78
- onKey: (stackLength) => {
79
- if (stackLength <= maxPathArrayLength && stackLength >= minPathArrayLength) updateMatches("key");
80
- },
81
- onPop: (stackLength) => {
82
- if (stackLength < maxPathArrayLength && stackLength >= minPathArrayLength - 1) {
83
- for (const info of jsonPathInfos) if (info.matches !== "unknown" && stackLength < info.pathArray.length) {
84
- info.matches = "unknown";
85
- jsonPathInfosThatMatch.delete(info);
86
- }
87
- }
88
- },
89
- onPush: (stackLength) => {
90
- if (stackLength <= maxPathArrayLength && stackLength >= minPathArrayLength) updateMatches("push");
54
+ parseWholeValue: () => !levels[parser.stack.length].matchesBelow,
55
+ onKey: updateLevel,
56
+ onPush: updateLevel,
57
+ onPop: (depth) => {
58
+ levels.length = depth + 1;
91
59
  },
92
60
  onValue: (value) => {
93
- let keep = false;
94
- for (const { key, path, pathArray, validate, wildcardIndexes } of jsonPathInfosThatMatch) if (parser.stack.length === pathArray.length) {
61
+ const depth = parser.stack.length;
62
+ const level = levels[depth];
63
+ if (level.matchesHere) for (const { key, path, pathArray, validate, wildcardIndexes } of level.jsonPathInfos) {
64
+ if (pathArray.length !== depth) continue;
95
65
  let valueToEmit;
96
66
  if (validate) {
97
67
  const result = validate(value);
@@ -116,8 +86,8 @@ var JSONParseStream = class extends TransformStream {
116
86
  key: key ?? path,
117
87
  value: valueToEmit
118
88
  });
119
- } else keep = true;
120
- if (keep) return;
89
+ }
90
+ if (level.matchesAbove) return;
121
91
  const type = typeof value;
122
92
  if (!(type === "string" || type === "number" || type === "boolean" || value === null)) {
123
93
  for (const row of parser.stack) row.value = void 0;
@@ -129,9 +99,8 @@ var JSONParseStream = class extends TransformStream {
129
99
  transform(chunk) {
130
100
  parser.write(chunk);
131
101
  },
132
- flush(controller) {
102
+ flush() {
133
103
  parser.checkEnd();
134
- controller.terminate();
135
104
  }
136
105
  });
137
106
  this._parser = parser;
@@ -1,7 +1,7 @@
1
1
  //#region src/JSONParseStreamRaw.d.ts
2
2
  type Token = "LEFT_BRACE" | "RIGHT_BRACE" | "LEFT_BRACKET" | "RIGHT_BRACKET" | "COLON" | "COMMA" | "TRUE" | "FALSE" | "NULL" | "STRING" | "NUMBER";
3
3
  type ParserState = "VALUE" | "KEY" | "VALUE_AFTER_COMMA" | "KEY_AFTER_COMMA";
4
- type TokenizerState = "START" | "TRUE1" | "TRUE2" | "TRUE3" | "FALSE1" | "FALSE2" | "FALSE3" | "FALSE4" | "NULL1" | "NULL2" | "NULL3" | "NUMBER-" | "NUMBER0" | "NUMBER" | "STRING1" | "STRING2" | "STRING3" | "STRING4" | "STRING5" | "STRING6";
4
+ type TokenizerState = "START" | "TRUE1" | "TRUE2" | "TRUE3" | "FALSE1" | "FALSE2" | "FALSE3" | "FALSE4" | "NULL1" | "NULL2" | "NULL3" | "NUMBER-" | "NUMBER0" | "NUMBER" | "NUMBER_DOT" | "NUMBER_FRACTION" | "NUMBER_E" | "NUMBER_E_SIGN" | "NUMBER_EXPONENT" | "STRING1" | "STRING2" | "STRING3" | "STRING4" | "STRING5" | "STRING6";
5
5
  type Mode = "OBJECT" | "ARRAY";
6
6
  type Key = string | number;
7
7
  type Value = any;
@@ -26,32 +26,45 @@ declare class JSONParseStreamRaw {
26
26
  onPush: OnPopPush | undefined;
27
27
  onValue: OnValue;
28
28
  unicode: string | undefined;
29
- highSurrogate: number | undefined;
30
29
  seenRootObject: boolean;
31
30
  multi: boolean | undefined;
32
31
  multiIndex: number;
32
+ parseWholeValue: (() => boolean) | undefined;
33
+ captureDisabled: boolean;
34
+ capturePieces: string[] | undefined;
35
+ captureStart: number;
36
+ captureDepth: number;
37
+ captureInString: boolean;
38
+ captureEscaped: boolean;
33
39
  constructor({
34
40
  multi,
35
41
  onKey,
36
42
  onPop,
37
43
  onPush,
38
- onValue
44
+ onValue,
45
+ parseWholeValue
39
46
  }: {
40
47
  multi?: boolean;
41
48
  onKey?: OnPopPush;
42
49
  onPop?: OnPopPush;
43
50
  onPush?: OnPopPush;
44
51
  onValue: OnValue;
52
+ parseWholeValue?: () => boolean;
45
53
  });
46
54
  charError(char: string, i: number): void;
47
55
  parseError(token: Token, value: Value, i: number): void;
56
+ shouldCapture(): boolean;
57
+ startCapture(text: string, i: number): number;
58
+ scanCapture(text: string, start: number): number;
59
+ endCapture(json: string): void;
60
+ parseCaptureStrict(json: string): void;
48
61
  write(text: string): void;
49
- push(): void;
62
+ push(mode: Mode, value: object): void;
50
63
  pop(): void;
51
- setKey(key: number | string | undefined): void;
64
+ setKey(key: string): void;
52
65
  emit(value: Value): void;
53
66
  onToken(token: Token, value: Value, i: number): void;
54
- numberReviver(text: string, i: number): void;
67
+ endNumber(i: number): void;
55
68
  checkEnd(): void;
56
69
  }
57
70
  //#endregion
@@ -1,10 +1,5 @@
1
1
  //#region src/JSONParseStreamRaw.ts
2
- const WHITESPACE = new Set([
3
- " ",
4
- " ",
5
- "\n",
6
- "\r"
7
- ]);
2
+ const isWhitespace = (n) => n === " " || n === "\n" || n === "\r" || n === " ";
8
3
  var JSONParseStreamRaw = class {
9
4
  tokenizerState = "START";
10
5
  state = "VALUE";
@@ -19,16 +14,23 @@ var JSONParseStreamRaw = class {
19
14
  onPush;
20
15
  onValue;
21
16
  unicode;
22
- highSurrogate;
23
17
  seenRootObject = false;
24
18
  multi;
25
19
  multiIndex = 0;
26
- constructor({ multi, onKey, onPop, onPush, onValue }) {
20
+ parseWholeValue;
21
+ captureDisabled = false;
22
+ capturePieces;
23
+ captureStart = 0;
24
+ captureDepth = 0;
25
+ captureInString = false;
26
+ captureEscaped = false;
27
+ constructor({ multi, onKey, onPop, onPush, onValue, parseWholeValue }) {
27
28
  this.multi = multi;
28
29
  this.onKey = onKey;
29
30
  this.onPop = onPop;
30
31
  this.onPush = onPush;
31
32
  this.onValue = onValue;
33
+ this.parseWholeValue = parseWholeValue;
32
34
  }
33
35
  charError(char, i) {
34
36
  throw new Error(`Unexpected ${JSON.stringify(char)} at position ${this.position + i} in state ${this.tokenizerState}`);
@@ -36,13 +38,91 @@ var JSONParseStreamRaw = class {
36
38
  parseError(token, value, i) {
37
39
  throw new Error(`Unexpected ${token}${value ? `(${JSON.stringify(value)})` : ""} at position ${this.position + i} in state ${this.state}`);
38
40
  }
41
+ shouldCapture() {
42
+ return this.parseWholeValue !== void 0 && !this.captureDisabled && (this.state === "VALUE" || this.state === "VALUE_AFTER_COMMA") && this.parseWholeValue();
43
+ }
44
+ startCapture(text, i) {
45
+ this.captureStart = this.position + i;
46
+ this.captureDepth = 0;
47
+ this.captureInString = false;
48
+ this.captureEscaped = false;
49
+ const end = this.scanCapture(text, i);
50
+ if (end === -1) {
51
+ this.capturePieces = [text.slice(i)];
52
+ return text.length - 1;
53
+ }
54
+ this.endCapture(text.slice(i, end + 1));
55
+ return end;
56
+ }
57
+ scanCapture(text, start) {
58
+ const l = text.length;
59
+ let j = start;
60
+ if (this.captureEscaped && j < l) {
61
+ this.captureEscaped = false;
62
+ j += 1;
63
+ }
64
+ let depth = this.captureDepth;
65
+ let inString = this.captureInString;
66
+ for (; j < l; j++) {
67
+ const code = text.charCodeAt(j);
68
+ if (inString) {
69
+ if (code === 92) {
70
+ j += 1;
71
+ if (j === l) this.captureEscaped = true;
72
+ } else if (code === 34) inString = false;
73
+ } else if (code === 34) inString = true;
74
+ else if (code === 123 || code === 91) depth += 1;
75
+ else if (code === 125 || code === 93) {
76
+ depth -= 1;
77
+ if (depth === 0) return j;
78
+ }
79
+ }
80
+ this.captureDepth = depth;
81
+ this.captureInString = inString;
82
+ return -1;
83
+ }
84
+ endCapture(json) {
85
+ this.capturePieces = void 0;
86
+ let value;
87
+ try {
88
+ value = JSON.parse(json);
89
+ } catch {
90
+ this.parseCaptureStrict(json);
91
+ return;
92
+ }
93
+ if (this.stack.length === 0) this.seenRootObject = true;
94
+ if (this.value) this.value[this.key] = value;
95
+ this.emit(value);
96
+ }
97
+ parseCaptureStrict(json) {
98
+ const position = this.position;
99
+ this.position = this.captureStart;
100
+ this.captureDisabled = true;
101
+ this.write(json);
102
+ this.captureDisabled = false;
103
+ this.position = position;
104
+ }
39
105
  write(text) {
40
- for (let i = 0, l = text.length; i < l; i++) {
106
+ let i = 0;
107
+ if (this.capturePieces !== void 0) {
108
+ const end = this.scanCapture(text, 0);
109
+ if (end === -1) {
110
+ this.capturePieces.push(text);
111
+ this.position += text.length;
112
+ return;
113
+ }
114
+ this.capturePieces.push(text.slice(0, end + 1));
115
+ this.endCapture(this.capturePieces.join(""));
116
+ i = end + 1;
117
+ }
118
+ for (const l = text.length; i < l; i++) {
41
119
  const n = text[i];
42
- if (!this.multi && this.stack.length === 0 && this.seenRootObject && !WHITESPACE.has(n)) return this.charError(n, i);
43
- if (this.tokenizerState === "START") if (n === "{") this.onToken("LEFT_BRACE", "{", i);
120
+ if (!this.multi && this.stack.length === 0 && this.seenRootObject && !isWhitespace(n)) return this.charError(n, i);
121
+ if (this.tokenizerState === "START") if (n === "{") if (this.shouldCapture()) i = this.startCapture(text, i);
122
+ else this.onToken("LEFT_BRACE", "{", i);
44
123
  else if (n === "}") this.onToken("RIGHT_BRACE", "}", i);
45
- else if (n === "[") this.onToken("LEFT_BRACKET", "[", i);
124
+ else if (n === "[") if (this.shouldCapture()) i = this.startCapture(text, i);
125
+ else this.onToken("LEFT_BRACKET", "[", i);
46
126
  else if (n === "]") this.onToken("RIGHT_BRACKET", "]", i);
47
127
  else if (n === ":") this.onToken("COLON", ":", i);
48
128
  else if (n === ",") this.onToken("COMMA", ",", i);
@@ -61,15 +141,21 @@ var JSONParseStreamRaw = class {
61
141
  } else if (n >= "1" && n <= "9") {
62
142
  this.string = n;
63
143
  this.tokenizerState = "NUMBER";
64
- } else if (WHITESPACE.has(n)) {} else if (n === "␞" && this.multi && this.stack.length === 0) {} else return this.charError(n, i);
144
+ } else if (isWhitespace(n)) {} else if (n === "␞" && this.multi && this.stack.length === 0) {} else return this.charError(n, i);
65
145
  else if (this.tokenizerState === "STRING1") if (n === "\"") {
66
146
  this.tokenizerState = "START";
67
147
  this.onToken("STRING", this.string, i);
68
148
  this.string = void 0;
69
149
  } else if (n === "\\") this.tokenizerState = "STRING2";
70
150
  else {
71
- if (n.charCodeAt(0) <= 31) this.charError(n, i);
72
- this.string += n;
151
+ let j = i;
152
+ for (; j < l; j++) {
153
+ const code = text.charCodeAt(j);
154
+ if (code === 34 || code === 92) break;
155
+ if (code <= 31) return this.charError(text[j], j);
156
+ }
157
+ this.string += text.slice(i, j);
158
+ i = j - 1;
73
159
  }
74
160
  else if (this.tokenizerState === "STRING2") if (n === "\"") {
75
161
  this.string += "\"";
@@ -100,59 +186,75 @@ var JSONParseStreamRaw = class {
100
186
  this.tokenizerState = "STRING3";
101
187
  } else return this.charError(n, i);
102
188
  else if (this.tokenizerState === "STRING3" || this.tokenizerState === "STRING4" || this.tokenizerState === "STRING5" || this.tokenizerState === "STRING6") {
189
+ if (!(n >= "0" && n <= "9" || n >= "a" && n <= "f" || n >= "A" && n <= "F")) return this.charError(n, i);
103
190
  this.unicode += n;
104
191
  if (this.tokenizerState === "STRING3") this.tokenizerState = "STRING4";
105
192
  else if (this.tokenizerState === "STRING4") this.tokenizerState = "STRING5";
106
193
  else if (this.tokenizerState === "STRING5") this.tokenizerState = "STRING6";
107
194
  else if (this.tokenizerState === "STRING6") {
108
195
  const intVal = Number.parseInt(this.unicode, 16);
109
- if (Number.isNaN(intVal)) return this.charError(n, i);
110
196
  this.unicode = void 0;
111
- if (this.highSurrogate !== void 0 && intVal >= 56320 && intVal < 57344) {
112
- this.string += String.fromCharCode(this.highSurrogate, intVal);
113
- this.highSurrogate = void 0;
114
- } else if (this.highSurrogate === void 0 && intVal >= 55296 && intVal < 56320) this.highSurrogate = intVal;
115
- else {
116
- if (this.highSurrogate !== void 0) {
117
- this.string += String.fromCharCode(this.highSurrogate);
118
- this.highSurrogate = void 0;
119
- }
120
- this.string += String.fromCharCode(intVal);
121
- }
197
+ this.string += String.fromCharCode(intVal);
122
198
  this.tokenizerState = "STRING1";
123
199
  }
124
- } else if (this.tokenizerState === "NUMBER" || this.tokenizerState === "NUMBER-" || this.tokenizerState === "NUMBER0") {
125
- if (this.tokenizerState === "NUMBER0" && n >= "0" && n <= "9") return this.charError("0", i - 1);
126
- switch (n) {
127
- case "0":
128
- this.string += n;
129
- this.tokenizerState = this.tokenizerState === "NUMBER-" ? "NUMBER0" : "NUMBER";
130
- break;
131
- case "1":
132
- case "2":
133
- case "3":
134
- case "4":
135
- case "5":
136
- case "6":
137
- case "7":
138
- case "8":
139
- case "9":
140
- case ".":
141
- case "e":
142
- case "E":
143
- case "+":
144
- case "-":
145
- this.string += n;
146
- this.tokenizerState = "NUMBER";
147
- break;
148
- default:
149
- this.tokenizerState = "START";
150
- this.numberReviver(this.string, i);
151
- this.string = void 0;
152
- i--;
153
- break;
154
- }
155
- } else if (this.tokenizerState === "TRUE1") if (n === "r") this.tokenizerState = "TRUE2";
200
+ } else if (this.tokenizerState === "NUMBER") if (n >= "0" && n <= "9") this.string += n;
201
+ else if (n === ".") {
202
+ this.string += n;
203
+ this.tokenizerState = "NUMBER_DOT";
204
+ } else if (n === "e" || n === "E") {
205
+ this.string += n;
206
+ this.tokenizerState = "NUMBER_E";
207
+ } else {
208
+ this.endNumber(i);
209
+ i--;
210
+ }
211
+ else if (this.tokenizerState === "NUMBER_FRACTION") if (n >= "0" && n <= "9") this.string += n;
212
+ else if (n === "e" || n === "E") {
213
+ this.string += n;
214
+ this.tokenizerState = "NUMBER_E";
215
+ } else {
216
+ this.endNumber(i);
217
+ i--;
218
+ }
219
+ else if (this.tokenizerState === "NUMBER0") if (n >= "0" && n <= "9") return this.charError("0", i - 1);
220
+ else if (n === ".") {
221
+ this.string += n;
222
+ this.tokenizerState = "NUMBER_DOT";
223
+ } else if (n === "e" || n === "E") {
224
+ this.string += n;
225
+ this.tokenizerState = "NUMBER_E";
226
+ } else {
227
+ this.endNumber(i);
228
+ i--;
229
+ }
230
+ else if (this.tokenizerState === "NUMBER-") if (n === "0") {
231
+ this.string += n;
232
+ this.tokenizerState = "NUMBER0";
233
+ } else if (n >= "1" && n <= "9") {
234
+ this.string += n;
235
+ this.tokenizerState = "NUMBER";
236
+ } else return this.charError(n, i);
237
+ else if (this.tokenizerState === "NUMBER_DOT") if (n >= "0" && n <= "9") {
238
+ this.string += n;
239
+ this.tokenizerState = "NUMBER_FRACTION";
240
+ } else return this.charError(n, i);
241
+ else if (this.tokenizerState === "NUMBER_E") if (n >= "0" && n <= "9") {
242
+ this.string += n;
243
+ this.tokenizerState = "NUMBER_EXPONENT";
244
+ } else if (n === "+" || n === "-") {
245
+ this.string += n;
246
+ this.tokenizerState = "NUMBER_E_SIGN";
247
+ } else return this.charError(n, i);
248
+ else if (this.tokenizerState === "NUMBER_E_SIGN") if (n >= "0" && n <= "9") {
249
+ this.string += n;
250
+ this.tokenizerState = "NUMBER_EXPONENT";
251
+ } else return this.charError(n, i);
252
+ else if (this.tokenizerState === "NUMBER_EXPONENT") if (n >= "0" && n <= "9") this.string += n;
253
+ else {
254
+ this.endNumber(i);
255
+ i--;
256
+ }
257
+ else if (this.tokenizerState === "TRUE1") if (n === "r") this.tokenizerState = "TRUE2";
156
258
  else return this.charError(n, i);
157
259
  else if (this.tokenizerState === "TRUE2") if (n === "u") this.tokenizerState = "TRUE3";
158
260
  else return this.charError(n, i);
@@ -181,12 +283,16 @@ var JSONParseStreamRaw = class {
181
283
  }
182
284
  this.position += text.length;
183
285
  }
184
- push() {
286
+ push(mode, value) {
185
287
  this.stack.push({
186
288
  value: this.value,
187
289
  key: this.key,
188
290
  mode: this.mode
189
291
  });
292
+ if (this.value) this.value[this.key] = value;
293
+ this.value = value;
294
+ this.mode = mode;
295
+ this.key = mode === "ARRAY" ? 0 : void 0;
190
296
  this.onPush?.(this.stack.length);
191
297
  }
192
298
  pop() {
@@ -201,7 +307,7 @@ var JSONParseStreamRaw = class {
201
307
  }
202
308
  setKey(key) {
203
309
  this.key = key;
204
- if (this.onKey && typeof key === "string" && this.mode === "OBJECT") this.onKey(this.stack.length);
310
+ this.onKey?.(this.stack.length);
205
311
  }
206
312
  emit(value) {
207
313
  if (this.mode) this.state = "COMMA";
@@ -216,18 +322,10 @@ var JSONParseStreamRaw = class {
216
322
  if (this.value) this.value[this.key] = value;
217
323
  this.emit(value);
218
324
  } else if (token === "LEFT_BRACE") {
219
- this.push();
220
- if (this.value) this.value = this.value[this.key] = {};
221
- else this.value = {};
222
- this.setKey(void 0);
325
+ this.push("OBJECT", {});
223
326
  this.state = "KEY";
224
- this.mode = "OBJECT";
225
327
  } else if (token === "LEFT_BRACKET") {
226
- this.push();
227
- if (this.value) this.value = this.value[this.key] = [];
228
- else this.value = [];
229
- this.setKey(0);
230
- this.mode = "ARRAY";
328
+ this.push("ARRAY", []);
231
329
  this.state = "VALUE";
232
330
  } else if (token === "RIGHT_BRACE") if (this.mode === "OBJECT" && this.state !== "VALUE_AFTER_COMMA") this.pop();
233
331
  else return this.parseError(token, value, i);
@@ -250,17 +348,22 @@ var JSONParseStreamRaw = class {
250
348
  else return this.parseError(token, value, i);
251
349
  else return this.parseError(token, value, i);
252
350
  }
253
- numberReviver(text, i) {
254
- const number = JSON.parse(text);
255
- if (Number.isNaN(number)) return this.charError(text, i);
351
+ endNumber(i) {
352
+ const number = Number(this.string);
353
+ this.string = void 0;
354
+ this.tokenizerState = "START";
256
355
  this.onToken("NUMBER", number, i);
257
356
  }
258
357
  checkEnd() {
358
+ if (this.capturePieces !== void 0) {
359
+ const json = this.capturePieces.join("");
360
+ this.capturePieces = void 0;
361
+ this.parseCaptureStrict(json);
362
+ }
259
363
  if (this.stack.length > 0) throw new Error(`Unexpected end of input at position ${this.position} in state ${this.state}`);
260
- if (this.state === "VALUE" && this.tokenizerState === "NUMBER" && this.string !== void 0) {
261
- this.numberReviver(this.string, this.position - 1);
262
- this.string = void 0;
263
- } else if (!this.seenRootObject) throw new Error("No data in input");
364
+ if (this.tokenizerState === "NUMBER" || this.tokenizerState === "NUMBER0" || this.tokenizerState === "NUMBER_FRACTION" || this.tokenizerState === "NUMBER_EXPONENT") this.endNumber(this.position - 1);
365
+ if (this.tokenizerState !== "START") throw new Error(`Unexpected end of input at position ${this.position} in state ${this.tokenizerState}`);
366
+ if (!this.seenRootObject) throw new Error("No data in input");
264
367
  }
265
368
  };
266
369
 
@@ -8,22 +8,23 @@ const jsonPathToPathArray = (path) => {
8
8
  } catch (error) {
9
9
  throw new Error(`Error parsing JSONPath "${path}"`, { cause: error });
10
10
  }
11
- return parsed.segments.flatMap((segment) => {
11
+ return parsed.segments.map((segment) => {
12
12
  if (segment.type === "ChildSegment") {
13
13
  const node = segment.node;
14
14
  if (node.type === "MemberNameShorthand") return {
15
15
  type: "key",
16
16
  value: node.value
17
17
  };
18
- else if (node.type === "BracketedSelection") return node.selectors.map((selector) => {
18
+ else if (node.type === "BracketedSelection") {
19
+ if (node.selectors.length !== 1) throw new Error(`Multiple selectors in brackets are not supported in JSONPath "${path}"`);
20
+ const selector = node.selectors[0];
19
21
  if (selector.type === "NameSelector") return {
20
22
  type: "key",
21
23
  value: selector.value
22
24
  };
23
25
  else if (selector.type === "WildcardSelector") return { type: "wildcard" };
24
26
  else throw new Error(`Unsupported node: ${JSON.stringify(node)}`);
25
- });
26
- else if (node.type === "WildcardSelector") return { type: "wildcard" };
27
+ } else if (node.type === "WildcardSelector") return { type: "wildcard" };
27
28
  else throw new Error(`${segment.type} node type not supported`);
28
29
  } else throw new Error(`${segment.type} segment type not supported`);
29
30
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-web-streams",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Streaming JSON parser built on top of the Web Streams API, so it works in web browsers, Node.js, and many other environments",
5
5
  "keywords": [
6
6
  "web streams",
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "license": "Apache-2.0",
16
16
  "author": "Jeremy Scheff <jeremy@zengm.com> (https://zengm.com/)",
17
+ "sideEffects": false,
17
18
  "type": "module",
18
19
  "main": "dist/index.js",
19
20
  "types": "dist/index.d.ts",
@@ -21,7 +22,7 @@
21
22
  "dist"
22
23
  ],
23
24
  "scripts": {
24
- "bench": "vitest bench",
25
+ "bench": "vitest bench --run",
25
26
  "build": "tsdown --unbundle",
26
27
  "format": "prettier --write .",
27
28
  "lint": "npm-run-all --parallel lint:oxlint lint:tsc",
@@ -29,7 +30,7 @@
29
30
  "lint:tsc": "tsc",
30
31
  "prepack": "node --run build",
31
32
  "prepare": "husky",
32
- "test": "vitest"
33
+ "test": "vitest --run"
33
34
  },
34
35
  "lint-staged": {
35
36
  "*.{js,cjs,mjs,jsx,json,scss,ts,cts,mts,tsx,md}": "prettier --write"
@@ -42,19 +43,19 @@
42
43
  "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
43
44
  "@types/node": "^24.9.1",
44
45
  "husky": "^9.1.7",
45
- "lint-staged": "^16.2.5",
46
+ "lint-staged": "^16.2.6",
46
47
  "npm-run-all2": "^8.0.4",
47
- "oxlint": "^1.23.0",
48
- "oxlint-tsgolint": "^0.2.0",
48
+ "oxlint": "^1.24.0",
49
+ "oxlint-tsgolint": "^0.3.0",
49
50
  "prettier": "^3.6.2",
50
51
  "prettier-plugin-packagejson": "^2.5.19",
51
52
  "tsdown": "^0.15.9",
52
53
  "typescript": "^5.9.3",
53
- "vitest": "^3.2.4",
54
+ "vitest": "^4.0.3",
54
55
  "zod": "^4.1.12"
55
56
  },
56
57
  "engines": {
57
58
  "node": ">=22",
58
- "pnpm": "^10.0.0"
59
+ "pnpm": "^11.0.0"
59
60
  }
60
61
  }