json-web-streams 0.0.1 → 1.1.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
@@ -1,5 +1,11 @@
1
1
  # json-web-streams
2
2
 
3
+ [![Build Status](https://github.com/zengm-games/json-web-streams/actions/workflows/test.yaml/badge.svg)](https://github.com/zengm-games/json-web-streams/actions/workflows/test.yaml)
4
+ [![GitHub Repo stars](https://img.shields.io/github/stars/zengm-games/json-web-streams)](https://github.com/zengm-games/json-web-streams)
5
+ [![GitHub](https://img.shields.io/github/license/zengm-games/json-web-streams)](https://github.com/zengm-games/json-web-streams)
6
+ [![npm](https://img.shields.io/npm/v/json-web-streams)](https://www.npmjs.com/package/json-web-streams)
7
+ [![npm](https://img.shields.io/npm/dm/json-web-streams)](https://www.npmjs.com/package/json-web-streams)
8
+
3
9
  - **Stream large JSON files** without loading everything into memory
4
10
  - Built on the **Web Streams API** so it runs in web browsers, Node.js, and more
5
11
  - Query with **JSONPath** to extract only the data you need
@@ -62,12 +68,12 @@ await response.body
62
68
 
63
69
  ```ts
64
70
  const jsonParseStream = new JSONParseStream(
65
- jsonPaths: (JSONPath | { path: JSONPath; schema: StandardSchemaV1 })[],
71
+ jsonPaths: (JSONPath | { path: JSONPath; key?: Key; schema?: StandardSchemaV1 })[],
66
72
  options?: { multi?: boolean },
67
73
  );
68
74
  ```
69
75
 
70
- ### `jsonPaths: (JSONPath | { path: JSONPath; schema: StandardSchemaV1 })[]`
76
+ ### `jsonPaths: (JSONPath | { path: JSONPath; key?: Key; schema?: StandardSchemaV1 })[]`
71
77
 
72
78
  The first argument to `JSONParseStream` is an array specifying what objects to emit from the stream. The `JSONPath` type is a string containing a JSONPath query. JSONPath is a query language for JSON to let you pick out specific values from a JSON object.
73
79
 
@@ -96,7 +102,15 @@ but you can have as many as you want:
96
102
  >
97
103
  > In total this query means "emit every value in the array/object in the `foo` property of the overall JSON object". So for this JSON `{ foo: ["A", "B", "C"] }` it would emit the three values `"A"`, `"B"`, and `"C"`.
98
104
 
99
- The values of the `jsonPaths` array can either be `JSONPath` strings, or objects like `{ path: JSONPath; schema: StandardSchemaV1 }` where `schema` is a schema validator from any library supporting the [Standard Schema specification](https://github.com/standard-schema/standard-schema) such as Zod, Valibot, or ArkType. When you supply a schema like this, each value will be validated before it is emitted by the stream, and emitted values will have correct TypeScript types rather than being `unknown`. For more details, see the [Schema validation and types for `JSONParseStream` output](#schema-validation-and-types-for-jsonparsestream-output) section below.
105
+ The values of the `jsonPaths` array can either be `JSONPath` strings, or objects like `{ path: JSONPath; key?: Key; schema?: StandardSchemaV1 }` to specify the additional optional `key` and `schema` properties.
106
+
107
+ #### `key?: Key`
108
+
109
+ If `key` is defined, it will be propagated through to the `key` property on output objects. So it can be any type, but typically is probably something like a number, string, or Symbol that you want to use to discriminate between types of outputs when you have multiple entries in the `jsonPaths` array. When `key` is not defined, then the value of `path` is used instead. See the [`JSONParseStream` output](#jsonparsestream-output) section below for examples.
110
+
111
+ #### `schema?: StandardSchemaV1`
112
+
113
+ `schema` is a schema validator from any library supporting the [Standard Schema specification](https://github.com/standard-schema/standard-schema) such as Zod, Valibot, or ArkType. When you supply a schema like this, each value will be validated before it is emitted by the stream, and emitted values will have correct TypeScript types rather than being `unknown`. For more details, see the [Schema validation and types for `JSONParseStream` output](#schema-validation-and-types-for-jsonparsestream-output) section below.
100
114
 
101
115
  ### `options?: { multi?: boolean }`
102
116
 
@@ -131,16 +145,16 @@ Output from `JSONParseStream` has this format:
131
145
  ```ts
132
146
  type JSONParseStreamOutput<T = unknown> = {
133
147
  value: T;
134
- path: JSONPath;
148
+ key: Key;
135
149
  wildcardKeys?: string[];
136
150
  };
137
151
  ```
138
152
 
139
153
  `value` is the value selected by one of your JSONPath queries.
140
154
 
141
- `path` is the JSONPath query (from the `jsonPaths` parameter of `JSONParseStream`) that matched `value`.
155
+ `key` is the JSONPath query (from the `jsonPaths` parameter of `JSONParseStream`) that matched `value`, or the value from the `key` property inside `jsonPaths` if that was supplied.
142
156
 
143
- If you only have one JSONPath query, you can ignore `path`. But if you have more than one, `path` may be helpful when processing stream output to distinguish between different types of values. For example:
157
+ If you only have one JSONPath query, you can ignore `key`. But if you have more than one, `key` may be helpful when processing stream output to distinguish between different types of values. For example:
144
158
 
145
159
  <!-- prettier-ignore -->
146
160
  ```ts
@@ -154,7 +168,31 @@ await new ReadableStream({
154
168
  .pipeTo(
155
169
  new WritableStream({
156
170
  write(record) {
157
- if (record.path === "$.bar[*]") {
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
+ );
179
+ ```
180
+
181
+ Or with a manually defined key:
182
+
183
+ <!-- prettier-ignore -->
184
+ ```ts
185
+ await new ReadableStream({
186
+ start(controller) {
187
+ controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
188
+ controller.close();
189
+ },
190
+ })
191
+ .pipeThrough(new JSONParseStream([{ key: "bar", path: "$.bar[*]" }, "$.foo[*]"]))
192
+ .pipeTo(
193
+ new WritableStream({
194
+ write(record) {
195
+ if (record.key === "bar") {
158
196
  // Do something with the values from bar
159
197
  } else {
160
198
  // Do something with the values from foo
@@ -183,8 +221,8 @@ await new ReadableStream({
183
221
  }),
184
222
  );
185
223
  // Output:
186
- // { path: "$[*]", value: [1, 2], wildcardKeys: ["foo"] },
187
- // { path: "$[*]", value: ["a", "b", "c"], wildcardKeys: ["bar"] },
224
+ // { key: "$[*]", value: [1, 2], wildcardKeys: ["foo"] },
225
+ // { key: "$[*]", value: ["a", "b", "c"], wildcardKeys: ["bar"] },
188
226
  ```
189
227
 
190
228
  The purpose of `wildcardKeys` is to allow you to easily distinguish different types of objects. `wildcardKeys` has one entry for each wildcard object in your JSONPath query.
@@ -219,7 +257,7 @@ await new ReadableStream({
219
257
  .pipeTo(
220
258
  new WritableStream({
221
259
  write(record) {
222
- if (record.path === "$.foo[*]") {
260
+ if (record.key === "$.foo[*]") {
223
261
  // Type of record.value is number
224
262
  } else {
225
263
  // Type of record.value is string
@@ -229,11 +267,42 @@ await new ReadableStream({
229
267
  );
230
268
  ```
231
269
 
232
- > [!TIP]
233
- > If you only want to validate some values, you can mix `{ path: JSONPath; schema: StandardSchemaV1 }` and `JSONPath` in the `jsonPaths` array.
234
-
235
270
  For JSONPath queries with no schema, emitted values will have the `unknown` type.
236
271
 
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:
273
+
274
+ <!-- prettier-ignore -->
275
+ ```ts
276
+ import * as z from "zod";
277
+
278
+ await new ReadableStream({
279
+ start(controller) {
280
+ controller.enqueue('{ "foo": [1, 2], "bar": ["a", "b", "c"] }');
281
+ controller.close();
282
+ },
283
+ })
284
+ .pipeThrough(
285
+ new JSONParseStream([
286
+ { key: "foo", path: "$.foo[*]", schema: z.number() },
287
+ { key: "bar", path: "$.bar[*]", schema: z.string() },
288
+ ]),
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
+ );
301
+ ```
302
+
303
+ > [!TIP]
304
+ > If you only want to validate values or override keys for some queries, you can mix `{ path: JSONPath; key?: Key; schema?: StandardSchemaV1 }` and `JSONPath` in the `jsonPaths` array.
305
+
237
306
  ## JSONPath
238
307
 
239
308
  json-web-streams supports a subset of JSONPath. Currently the supported components are:
@@ -279,7 +348,7 @@ Let's say you have this JSON:
279
348
  { "foo": [1, 2], "bar": ["a", "b", "c"] }
280
349
  ```
281
350
 
282
- You want to get all the values in `foo` and all the values in `bar`. You could define them as two separate JSONPath queries and then distinguish the output with `.path`:
351
+ You want to get all the values in `foo` and all the values in `bar`. You could define them as two separate JSONPath queries and then distinguish the output with `.key`:
283
352
 
284
353
  <!-- prettier-ignore -->
285
354
  ```ts
@@ -295,7 +364,7 @@ await new ReadableStream({
295
364
  .pipeTo(
296
365
  new WritableStream({
297
366
  write(record) {
298
- if (record.path === "$.foo[*]") {
367
+ if (record.key === "$.foo[*]") {
299
368
  // 1, 2
300
369
  } else {
301
370
  // a, b, c
@@ -356,7 +425,7 @@ await new ReadableStream({
356
425
  .pipeTo(
357
426
  new WritableStream({
358
427
  write(record) {
359
- if (record.path === "$.foo[*]") {
428
+ if (record.key === "$.foo[*]") {
360
429
  // 1, 2
361
430
  } else {
362
431
  // a, b, c
@@ -0,0 +1,33 @@
1
+ import { JSONParseStreamRaw } from "./JSONParseStreamRaw.js";
2
+ import { JSONPath } from "./jsonPathToPathArray.js";
3
+ import { StandardSchemaV1 } from "@standard-schema/spec";
4
+
5
+ //#region src/JSONParseStream.d.ts
6
+ type JSONParseStreamOutput<T> = T extends {
7
+ key?: infer K | undefined;
8
+ path: infer P extends JSONPath;
9
+ schema?: infer S extends StandardSchemaV1 | undefined;
10
+ } ? {
11
+ value: S extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<S> : unknown;
12
+ wildcardKeys?: string[];
13
+ } & (undefined extends K ? {
14
+ key: P;
15
+ } : {
16
+ key: K;
17
+ }) : T extends JSONPath ? {
18
+ key: T;
19
+ value: unknown;
20
+ wildcardKeys?: string[];
21
+ } : never;
22
+ declare class JSONParseStream<const Key extends unknown, T extends readonly (JSONPath | {
23
+ key?: Key;
24
+ path: JSONPath;
25
+ schema?: StandardSchemaV1;
26
+ })[]> extends TransformStream<string, JSONParseStreamOutput<T[number]>> {
27
+ _parser: JSONParseStreamRaw;
28
+ constructor(jsonPaths: T, options?: {
29
+ multi?: boolean;
30
+ });
31
+ }
32
+ //#endregion
33
+ export { JSONParseStream };
@@ -0,0 +1,142 @@
1
+ import { JSONParseStreamRaw } from "./JSONParseStreamRaw.js";
2
+ import { jsonPathToPathArray } from "./jsonPathToPathArray.js";
3
+
4
+ //#region src/JSONParseStream.ts
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;
12
+ };
13
+ var JSONParseStream = class extends TransformStream {
14
+ _parser;
15
+ constructor(jsonPaths, options) {
16
+ let parser;
17
+ let minPathArrayLength = Infinity;
18
+ let maxPathArrayLength = -Infinity;
19
+ const jsonPathInfos = jsonPaths.map((row) => {
20
+ let key;
21
+ let path;
22
+ let schema;
23
+ if (typeof row === "string") path = row;
24
+ else {
25
+ key = row.key;
26
+ path = row.path;
27
+ schema = row.schema;
28
+ }
29
+ const pathArray = jsonPathToPathArray(path);
30
+ let wildcardIndexes;
31
+ for (const [i, component] of pathArray.entries()) if (component.type === "wildcard") {
32
+ if (wildcardIndexes === void 0) wildcardIndexes = [];
33
+ wildcardIndexes.push(i);
34
+ }
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
+ return {
39
+ key,
40
+ matches,
41
+ path,
42
+ pathArray,
43
+ validate: schema?.["~standard"].validate,
44
+ wildcardIndexes
45
+ };
46
+ });
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
+ }
73
+ };
74
+ super({
75
+ start(controller) {
76
+ parser = new JSONParseStreamRaw({
77
+ 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");
91
+ },
92
+ onValue: (value) => {
93
+ let keep = false;
94
+ for (const { key, path, pathArray, validate, wildcardIndexes } of jsonPathInfosThatMatch) if (parser.stack.length === pathArray.length) {
95
+ let valueToEmit;
96
+ if (validate) {
97
+ const result = validate(value);
98
+ if (result instanceof Promise) throw new TypeError("Schema validation must be synchronous");
99
+ if (result.issues) throw new Error(JSON.stringify(result.issues, null, 2));
100
+ valueToEmit = result.value;
101
+ } else valueToEmit = value;
102
+ let wildcardKeys;
103
+ if (wildcardIndexes) for (const index of wildcardIndexes) {
104
+ const stackComponent = parser.stack[index + 1] ?? parser;
105
+ if (stackComponent.mode === "OBJECT" && stackComponent.key !== void 0) {
106
+ if (!wildcardKeys) wildcardKeys = [];
107
+ wildcardKeys.push(stackComponent.key);
108
+ }
109
+ }
110
+ if (wildcardKeys) controller.enqueue({
111
+ key: key ?? path,
112
+ value: valueToEmit,
113
+ wildcardKeys
114
+ });
115
+ else controller.enqueue({
116
+ key: key ?? path,
117
+ value: valueToEmit
118
+ });
119
+ } else keep = true;
120
+ if (keep) return;
121
+ const type = typeof value;
122
+ if (!(type === "string" || type === "number" || type === "boolean" || value === null)) {
123
+ for (const row of parser.stack) row.value = void 0;
124
+ if (typeof parser.value === "object" && parser.value !== null && parser.key !== void 0) parser.value[parser.key] = void 0;
125
+ }
126
+ }
127
+ });
128
+ },
129
+ transform(chunk) {
130
+ parser.write(chunk);
131
+ },
132
+ flush(controller) {
133
+ parser.checkEnd();
134
+ controller.terminate();
135
+ }
136
+ });
137
+ this._parser = parser;
138
+ }
139
+ };
140
+
141
+ //#endregion
142
+ export { JSONParseStream };
@@ -1,4 +1,4 @@
1
- //#region src/JSONParserText.d.ts
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
4
  type TokenizerState = "START" | "TRUE1" | "TRUE2" | "TRUE3" | "FALSE1" | "FALSE2" | "FALSE3" | "FALSE4" | "NULL1" | "NULL2" | "NULL3" | "NUMBER-" | "NUMBER0" | "NUMBER" | "STRING1" | "STRING2" | "STRING3" | "STRING4" | "STRING5" | "STRING6";
@@ -10,8 +10,9 @@ type Stack = {
10
10
  value: Value | undefined;
11
11
  mode: Mode | undefined;
12
12
  }[];
13
- type OnValue = (value: Value, stack: Stack) => void;
14
- declare class JSONParserText {
13
+ type OnPopPush = (stackLength: number) => void;
14
+ type OnValue = (value: Value) => void;
15
+ declare class JSONParseStreamRaw {
15
16
  tokenizerState: TokenizerState;
16
17
  state: Token | ParserState;
17
18
  mode: Mode | undefined;
@@ -20,17 +21,26 @@ declare class JSONParserText {
20
21
  key: Key | undefined;
21
22
  value: Value;
22
23
  position: number;
24
+ onKey: OnPopPush | undefined;
25
+ onPop: OnPopPush | undefined;
26
+ onPush: OnPopPush | undefined;
23
27
  onValue: OnValue;
24
28
  unicode: string | undefined;
25
29
  highSurrogate: number | undefined;
26
30
  seenRootObject: boolean;
27
- multi: boolean;
31
+ multi: boolean | undefined;
28
32
  multiIndex: number;
29
33
  constructor({
30
34
  multi,
35
+ onKey,
36
+ onPop,
37
+ onPush,
31
38
  onValue
32
39
  }: {
33
- multi: boolean;
40
+ multi?: boolean;
41
+ onKey?: OnPopPush;
42
+ onPop?: OnPopPush;
43
+ onPush?: OnPopPush;
34
44
  onValue: OnValue;
35
45
  });
36
46
  charError(char: string, i: number): void;
@@ -38,10 +48,11 @@ declare class JSONParserText {
38
48
  write(text: string): void;
39
49
  push(): void;
40
50
  pop(): void;
51
+ setKey(key: number | string | undefined): void;
41
52
  emit(value: Value): void;
42
53
  onToken(token: Token, value: Value, i: number): void;
43
54
  numberReviver(text: string, i: number): void;
44
55
  checkEnd(): void;
45
56
  }
46
57
  //#endregion
47
- export { JSONParserText };
58
+ export { JSONParseStreamRaw };
@@ -1,11 +1,11 @@
1
- //#region src/JSONParserText.ts
1
+ //#region src/JSONParseStreamRaw.ts
2
2
  const WHITESPACE = new Set([
3
3
  " ",
4
4
  " ",
5
5
  "\n",
6
6
  "\r"
7
7
  ]);
8
- var JSONParserText = class {
8
+ var JSONParseStreamRaw = class {
9
9
  tokenizerState = "START";
10
10
  state = "VALUE";
11
11
  mode;
@@ -14,15 +14,21 @@ var JSONParserText = class {
14
14
  key;
15
15
  value;
16
16
  position = 0;
17
+ onKey;
18
+ onPop;
19
+ onPush;
17
20
  onValue;
18
21
  unicode;
19
22
  highSurrogate;
20
23
  seenRootObject = false;
21
24
  multi;
22
25
  multiIndex = 0;
23
- constructor({ multi, onValue }) {
24
- this.onValue = onValue;
26
+ constructor({ multi, onKey, onPop, onPush, onValue }) {
25
27
  this.multi = multi;
28
+ this.onKey = onKey;
29
+ this.onPop = onPop;
30
+ this.onPush = onPush;
31
+ this.onValue = onValue;
26
32
  }
27
33
  charError(char, i) {
28
34
  throw new Error(`Unexpected ${JSON.stringify(char)} at position ${this.position + i} in state ${this.tokenizerState}`);
@@ -34,7 +40,6 @@ var JSONParserText = class {
34
40
  for (let i = 0, l = text.length; i < l; i++) {
35
41
  const n = text[i];
36
42
  if (!this.multi && this.stack.length === 0 && this.seenRootObject && !WHITESPACE.has(n)) return this.charError(n, i);
37
- if (this.multi && this.stack.length === 0 && this.seenRootObject) {}
38
43
  if (this.tokenizerState === "START") if (n === "{") this.onToken("LEFT_BRACE", "{", i);
39
44
  else if (n === "}") this.onToken("RIGHT_BRACE", "}", i);
40
45
  else if (n === "[") this.onToken("LEFT_BRACKET", "[", i);
@@ -182,6 +187,7 @@ var JSONParserText = class {
182
187
  key: this.key,
183
188
  mode: this.mode
184
189
  });
190
+ this.onPush?.(this.stack.length);
185
191
  }
186
192
  pop() {
187
193
  const value = this.value;
@@ -190,15 +196,17 @@ var JSONParserText = class {
190
196
  this.key = parent.key;
191
197
  this.mode = parent.mode;
192
198
  this.emit(value);
199
+ this.onPop?.(this.stack.length);
193
200
  if (!this.mode) this.state = "VALUE";
194
201
  }
202
+ setKey(key) {
203
+ this.key = key;
204
+ if (this.onKey && typeof key === "string" && this.mode === "OBJECT") this.onKey(this.stack.length);
205
+ }
195
206
  emit(value) {
196
207
  if (this.mode) this.state = "COMMA";
197
- this.onValue(value, [...this.stack, {
198
- value: this.value,
199
- key: this.key,
200
- mode: this.mode
201
- }]);
208
+ if (value === void 0) return;
209
+ this.onValue(value);
202
210
  }
203
211
  onToken(token, value, i) {
204
212
  if (this.stack.length === 0) {
@@ -211,14 +219,14 @@ var JSONParserText = class {
211
219
  this.push();
212
220
  if (this.value) this.value = this.value[this.key] = {};
213
221
  else this.value = {};
214
- this.key = void 0;
222
+ this.setKey(void 0);
215
223
  this.state = "KEY";
216
224
  this.mode = "OBJECT";
217
225
  } else if (token === "LEFT_BRACKET") {
218
226
  this.push();
219
227
  if (this.value) this.value = this.value[this.key] = [];
220
228
  else this.value = [];
221
- this.key = 0;
229
+ this.setKey(0);
222
230
  this.mode = "ARRAY";
223
231
  this.state = "VALUE";
224
232
  } else if (token === "RIGHT_BRACE") if (this.mode === "OBJECT" && this.state !== "VALUE_AFTER_COMMA") this.pop();
@@ -227,7 +235,7 @@ var JSONParserText = class {
227
235
  else return this.parseError(token, value, i);
228
236
  else return this.parseError(token, value, i);
229
237
  else if (this.state === "KEY" || this.state === "KEY_AFTER_COMMA") if (token === "STRING") {
230
- this.key = value;
238
+ this.setKey(value);
231
239
  this.state = "COLON";
232
240
  } else if (token === "RIGHT_BRACE" && this.state !== "KEY_AFTER_COMMA") this.pop();
233
241
  else return this.parseError(token, value, i);
@@ -255,7 +263,6 @@ var JSONParserText = class {
255
263
  } else if (!this.seenRootObject) throw new Error("No data in input");
256
264
  }
257
265
  };
258
- var JSONParserText_default = JSONParserText;
259
266
 
260
267
  //#endregion
261
- export { JSONParserText_default as default };
268
+ export { JSONParseStreamRaw };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { JSONPath } from "./jsonPathToQueryPath.js";
2
- import { createJSONParserStream } from "./JSONParserStream.js";
3
- export { type JSONPath, createJSONParserStream };
1
+ import { JSONPath } from "./jsonPathToPathArray.js";
2
+ import { JSONParseStream } from "./JSONParseStream.js";
3
+ export { JSONParseStream, type JSONPath };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { createJSONParserStream } from "./JSONParserStream.js";
1
+ import { JSONParseStream } from "./JSONParseStream.js";
2
2
 
3
- export { createJSONParserStream };
3
+ export { JSONParseStream };
@@ -1,4 +1,4 @@
1
- //#region src/jsonPathToQueryPath.d.ts
1
+ //#region src/jsonPathToPathArray.d.ts
2
2
 
3
3
  type JSONPath = "$" | `$${"." | "["}${string}`;
4
4
  //#endregion
@@ -1,12 +1,12 @@
1
1
  import parser from "jsonpath-rfc9535/parser";
2
2
 
3
- //#region src/jsonPathToQueryPath.ts
4
- const jsonPathToQueryPath = (jsonPath) => {
3
+ //#region src/jsonPathToPathArray.ts
4
+ const jsonPathToPathArray = (path) => {
5
5
  let parsed;
6
6
  try {
7
- parsed = parser(jsonPath);
7
+ parsed = parser(path);
8
8
  } catch (error) {
9
- throw new Error(`Error parsing JSONPath "${jsonPath}"`, { cause: error });
9
+ throw new Error(`Error parsing JSONPath "${path}"`, { cause: error });
10
10
  }
11
11
  return parsed.segments.flatMap((segment) => {
12
12
  if (segment.type === "ChildSegment") {
@@ -30,4 +30,4 @@ const jsonPathToQueryPath = (jsonPath) => {
30
30
  };
31
31
 
32
32
  //#endregion
33
- export { jsonPathToQueryPath };
33
+ export { jsonPathToPathArray };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-web-streams",
3
- "version": "0.0.1",
3
+ "version": "1.1.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",
@@ -21,11 +21,13 @@
21
21
  "dist"
22
22
  ],
23
23
  "scripts": {
24
+ "bench": "vitest bench",
24
25
  "build": "tsdown --unbundle",
25
26
  "format": "prettier --write .",
26
27
  "lint": "npm-run-all --parallel lint:oxlint lint:tsc",
27
28
  "lint:oxlint": "oxlint --type-aware",
28
29
  "lint:tsc": "tsc",
30
+ "prepack": "node --run build",
29
31
  "prepare": "husky",
30
32
  "test": "vitest"
31
33
  },
@@ -38,15 +40,15 @@
38
40
  },
39
41
  "devDependencies": {
40
42
  "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
41
- "@types/node": "^24.8.1",
43
+ "@types/node": "^24.9.1",
42
44
  "husky": "^9.1.7",
43
- "lint-staged": "^16.2.4",
45
+ "lint-staged": "^16.2.5",
44
46
  "npm-run-all2": "^8.0.4",
45
47
  "oxlint": "^1.23.0",
46
48
  "oxlint-tsgolint": "^0.2.0",
47
49
  "prettier": "^3.6.2",
48
50
  "prettier-plugin-packagejson": "^2.5.19",
49
- "tsdown": "^0.15.7",
51
+ "tsdown": "^0.15.9",
50
52
  "typescript": "^5.9.3",
51
53
  "vitest": "^3.2.4",
52
54
  "zod": "^4.1.12"
@@ -1,18 +0,0 @@
1
- import { JSONParserText } from "./JSONParserText.js";
2
- import { JSONPath } from "./jsonPathToQueryPath.js";
3
- import { StandardSchemaV1 } from "@standard-schema/spec";
4
-
5
- //#region src/JSONParserStream.d.ts
6
- declare const createJSONParserStream: <JSONPathsObject extends Partial<Record<JSONPath, StandardSchemaV1 | null | undefined>> = Record<JSONPath, never>>(jsonPaths: JSONPathsObject) => JSONParserStream<JSONPathsObject>;
7
- declare class JSONParserStream<JSONPathsObject extends Partial<Record<JSONPath, StandardSchemaV1 | null | undefined>>> extends TransformStream<string, {
8
- jsonPath: JSONPath;
9
- value: unknown;
10
- wildcardKeys?: string[];
11
- }> {
12
- _parser: JSONParserText;
13
- constructor(jsonPaths: JSONPathsObject, options?: {
14
- multi?: boolean;
15
- });
16
- }
17
- //#endregion
18
- export { createJSONParserStream };
@@ -1,108 +0,0 @@
1
- import JSONParserText_default from "./JSONParserText.js";
2
- import { jsonPathToQueryPath } from "./jsonPathToQueryPath.js";
3
-
4
- //#region src/JSONParserStream.ts
5
- const stackToQueryPath = (stack) => {
6
- return stack.slice(1).map((row) => {
7
- if (row.mode === "OBJECT" && row.key !== void 0) return {
8
- type: "key",
9
- value: row.key
10
- };
11
- if (row.mode === "ARRAY") return { type: "wildcard" };
12
- throw new Error(`Unexpected mode "${row.mode}"`);
13
- });
14
- };
15
- const isEqual = (x, y) => {
16
- if (!y) return false;
17
- if (x.type === y?.type) {
18
- if (x.type === "wildcard") return true;
19
- if (x.value === y.value) return true;
20
- return false;
21
- }
22
- return x.type === "wildcard";
23
- };
24
- const createJSONParserStream = (jsonPaths) => {
25
- return new JSONParserStream(jsonPaths);
26
- };
27
- var JSONParserStream = class extends TransformStream {
28
- _parser;
29
- constructor(jsonPaths, options) {
30
- let parser;
31
- const multi = options?.multi ?? false;
32
- const queryInfos = /* @__PURE__ */ new Map();
33
- for (const [key, schema] of Object.entries(jsonPaths)) {
34
- const jsonPath = key;
35
- const queryPath = jsonPathToQueryPath(jsonPath);
36
- let wildcardIndexes;
37
- for (const [i, component] of queryPath.entries()) if (component.type === "wildcard") {
38
- if (wildcardIndexes === void 0) wildcardIndexes = [];
39
- wildcardIndexes.push(i);
40
- }
41
- queryInfos.set(jsonPath, {
42
- jsonPath,
43
- queryPath,
44
- schema,
45
- wildcardIndexes
46
- });
47
- }
48
- super({
49
- start(controller) {
50
- parser = new JSONParserText_default({
51
- multi,
52
- onValue: (value, stack) => {
53
- const path = stackToQueryPath(stack);
54
- let keep = false;
55
- for (const { jsonPath, queryPath, schema, wildcardIndexes } of queryInfos.values()) if (queryPath.every((x, j) => isEqual(x, path[j]))) if (path.length === queryPath.length) {
56
- let valueToEmit;
57
- if (schema) {
58
- const result = schema["~standard"].validate(value);
59
- if (result instanceof Promise) throw new Error("async schema validation is not supported");
60
- if (result.issues) throw new Error(JSON.stringify(result.issues, null, 2));
61
- valueToEmit = result.value;
62
- } else valueToEmit = queryInfos.size === 1 ? value : structuredClone(value);
63
- console.log("valueToEmit", value, valueToEmit);
64
- let wildcardKeys;
65
- if (wildcardIndexes) {
66
- if (wildcardIndexes) for (const index of wildcardIndexes) {
67
- const pathComponent = path[index];
68
- if (pathComponent?.type === "key") {
69
- if (!wildcardKeys) wildcardKeys = [];
70
- wildcardKeys.push(pathComponent.value);
71
- }
72
- }
73
- }
74
- if (wildcardKeys) controller.enqueue({
75
- jsonPath,
76
- value: valueToEmit,
77
- wildcardKeys
78
- });
79
- else controller.enqueue({
80
- jsonPath,
81
- value: valueToEmit
82
- });
83
- } else keep = true;
84
- else {
85
- const type = typeof value;
86
- if (type === "string" || type === "number" || type === "boolean" || value === null) keep = true;
87
- }
88
- if (!keep) {
89
- for (const row of parser.stack) row.value = void 0;
90
- if (typeof parser.value === "object" && parser.value !== null && parser.key !== void 0) delete parser.value[parser.key];
91
- }
92
- }
93
- });
94
- },
95
- transform(chunk) {
96
- parser.write(chunk);
97
- },
98
- flush(controller) {
99
- parser.checkEnd();
100
- controller.terminate();
101
- }
102
- });
103
- this._parser = parser;
104
- }
105
- };
106
-
107
- //#endregion
108
- export { createJSONParserStream };