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 +85 -16
- package/dist/JSONParseStream.d.ts +33 -0
- package/dist/JSONParseStream.js +142 -0
- package/dist/{JSONParserText.d.ts → JSONParseStreamRaw.d.ts} +17 -6
- package/dist/{JSONParserText.js → JSONParseStreamRaw.js} +22 -15
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/{jsonPathToQueryPath.d.ts → jsonPathToPathArray.d.ts} +1 -1
- package/dist/{jsonPathToQueryPath.js → jsonPathToPathArray.js} +5 -5
- package/package.json +6 -4
- package/dist/JSONParserStream.d.ts +0 -18
- package/dist/JSONParserStream.js +0 -108
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# json-web-streams
|
|
2
2
|
|
|
3
|
+
[](https://github.com/zengm-games/json-web-streams/actions/workflows/test.yaml)
|
|
4
|
+
[](https://github.com/zengm-games/json-web-streams)
|
|
5
|
+
[](https://github.com/zengm-games/json-web-streams)
|
|
6
|
+
[](https://www.npmjs.com/package/json-web-streams)
|
|
7
|
+
[](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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
`
|
|
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 `
|
|
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.
|
|
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
|
-
// {
|
|
187
|
-
// {
|
|
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.
|
|
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 `.
|
|
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.
|
|
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.
|
|
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/
|
|
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
|
|
14
|
-
|
|
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
|
|
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 {
|
|
58
|
+
export { JSONParseStreamRaw };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
//#region src/
|
|
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
|
|
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
|
-
|
|
198
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
268
|
+
export { JSONParseStreamRaw };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { JSONPath } from "./
|
|
2
|
-
import {
|
|
3
|
-
export { type JSONPath
|
|
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 {
|
|
1
|
+
import { JSONParseStream } from "./JSONParseStream.js";
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { JSONParseStream };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import parser from "jsonpath-rfc9535/parser";
|
|
2
2
|
|
|
3
|
-
//#region src/
|
|
4
|
-
const
|
|
3
|
+
//#region src/jsonPathToPathArray.ts
|
|
4
|
+
const jsonPathToPathArray = (path) => {
|
|
5
5
|
let parsed;
|
|
6
6
|
try {
|
|
7
|
-
parsed = parser(
|
|
7
|
+
parsed = parser(path);
|
|
8
8
|
} catch (error) {
|
|
9
|
-
throw new Error(`Error parsing JSONPath "${
|
|
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 {
|
|
33
|
+
export { jsonPathToPathArray };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "json-web-streams",
|
|
3
|
-
"version": "
|
|
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.
|
|
43
|
+
"@types/node": "^24.9.1",
|
|
42
44
|
"husky": "^9.1.7",
|
|
43
|
-
"lint-staged": "^16.2.
|
|
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.
|
|
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 };
|
package/dist/JSONParserStream.js
DELETED
|
@@ -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 };
|