json-web-streams 0.0.1 → 1.0.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 +6 -0
- package/dist/JSONParseStream.d.ts +28 -0
- package/dist/JSONParseStream.js +116 -0
- package/dist/{JSONParserText.d.ts → JSONParseStreamRaw.d.ts} +4 -4
- package/dist/{JSONParserText.js → JSONParseStreamRaw.js} +5 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/{jsonPathToQueryPath.d.ts → jsonPathToPathArray.d.ts} +1 -1
- package/dist/{jsonPathToQueryPath.js → jsonPathToPathArray.js} +5 -5
- package/package.json +2 -1
- 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
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
path: infer P extends JSONPath;
|
|
8
|
+
schema: infer S extends StandardSchemaV1;
|
|
9
|
+
} ? {
|
|
10
|
+
path: P;
|
|
11
|
+
value: StandardSchemaV1.InferOutput<S>;
|
|
12
|
+
wildcardKeys?: string[];
|
|
13
|
+
} : T extends JSONPath ? {
|
|
14
|
+
path: T;
|
|
15
|
+
value: unknown;
|
|
16
|
+
wildcardKeys?: string[];
|
|
17
|
+
} : never;
|
|
18
|
+
declare class JSONParseStream<T extends readonly (JSONPath | {
|
|
19
|
+
path: JSONPath;
|
|
20
|
+
schema: StandardSchemaV1;
|
|
21
|
+
})[]> extends TransformStream<string, JSONParseStreamOutput<T[number]>> {
|
|
22
|
+
_parser: JSONParseStreamRaw;
|
|
23
|
+
constructor(jsonPaths: T, options?: {
|
|
24
|
+
multi?: boolean;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { JSONParseStream };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { JSONParseStreamRaw } from "./JSONParseStreamRaw.js";
|
|
2
|
+
import { jsonPathToPathArray } from "./jsonPathToPathArray.js";
|
|
3
|
+
|
|
4
|
+
//#region src/JSONParseStream.ts
|
|
5
|
+
const stackToPathComponent = (stackComponent) => {
|
|
6
|
+
if (stackComponent.mode === "OBJECT" && stackComponent.key !== void 0) return {
|
|
7
|
+
type: "key",
|
|
8
|
+
value: stackComponent.key
|
|
9
|
+
};
|
|
10
|
+
if (stackComponent.mode === "ARRAY") return { type: "wildcard" };
|
|
11
|
+
throw new Error(`Unexpected mode "${stackComponent.mode}"`);
|
|
12
|
+
};
|
|
13
|
+
const isEqual = (x, y) => {
|
|
14
|
+
if (!y) return false;
|
|
15
|
+
if (x.type === y?.type) {
|
|
16
|
+
if (x.type === "wildcard") return true;
|
|
17
|
+
if (x.value === y.value) return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return x.type === "wildcard";
|
|
21
|
+
};
|
|
22
|
+
var JSONParseStream = class extends TransformStream {
|
|
23
|
+
_parser;
|
|
24
|
+
constructor(jsonPaths, options) {
|
|
25
|
+
let parser;
|
|
26
|
+
const multi = options?.multi ?? false;
|
|
27
|
+
const jsonPathInfos = jsonPaths.map((row) => {
|
|
28
|
+
let path;
|
|
29
|
+
let schema;
|
|
30
|
+
if (typeof row === "string") path = row;
|
|
31
|
+
else {
|
|
32
|
+
path = row.path;
|
|
33
|
+
schema = row.schema;
|
|
34
|
+
}
|
|
35
|
+
const pathArray = jsonPathToPathArray(path);
|
|
36
|
+
let wildcardIndexes;
|
|
37
|
+
for (const [i, component] of pathArray.entries()) if (component.type === "wildcard") {
|
|
38
|
+
if (wildcardIndexes === void 0) wildcardIndexes = [];
|
|
39
|
+
wildcardIndexes.push(i);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
path,
|
|
43
|
+
pathArray,
|
|
44
|
+
schema,
|
|
45
|
+
wildcardIndexes
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
super({
|
|
49
|
+
start(controller) {
|
|
50
|
+
parser = new JSONParseStreamRaw({
|
|
51
|
+
multi,
|
|
52
|
+
onValue: (value) => {
|
|
53
|
+
const { key: parserKey, mode: parserMode, stack: parserStack, value: parserValue } = parser;
|
|
54
|
+
const stackLength = parserStack.length;
|
|
55
|
+
const stackPathArray = new Array(stackLength);
|
|
56
|
+
for (let i = 1; i < stackLength; i++) stackPathArray[i - 1] = stackToPathComponent(parserStack[i]);
|
|
57
|
+
if (parserStack.length > 0) stackPathArray[stackLength - 1] = stackToPathComponent({
|
|
58
|
+
value: parserValue,
|
|
59
|
+
key: parserKey,
|
|
60
|
+
mode: parserMode
|
|
61
|
+
});
|
|
62
|
+
let keep = false;
|
|
63
|
+
for (const { path, pathArray, schema, wildcardIndexes } of jsonPathInfos) {
|
|
64
|
+
if (stackPathArray.length < pathArray.length) continue;
|
|
65
|
+
if (pathArray.every((x, j) => isEqual(x, stackPathArray[j]))) if (stackPathArray.length === pathArray.length) {
|
|
66
|
+
let valueToEmit;
|
|
67
|
+
if (schema) {
|
|
68
|
+
const result = schema["~standard"].validate(value);
|
|
69
|
+
if (result instanceof Promise) throw new TypeError("Schema validation must be synchronous");
|
|
70
|
+
if (result.issues) throw new Error(JSON.stringify(result.issues, null, 2));
|
|
71
|
+
valueToEmit = result.value;
|
|
72
|
+
} else valueToEmit = value;
|
|
73
|
+
let wildcardKeys;
|
|
74
|
+
if (wildcardIndexes) for (const index of wildcardIndexes) {
|
|
75
|
+
const pathComponent = stackPathArray[index];
|
|
76
|
+
if (pathComponent?.type === "key") {
|
|
77
|
+
if (!wildcardKeys) wildcardKeys = [];
|
|
78
|
+
wildcardKeys.push(pathComponent.value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (wildcardKeys) controller.enqueue({
|
|
82
|
+
path,
|
|
83
|
+
value: valueToEmit,
|
|
84
|
+
wildcardKeys
|
|
85
|
+
});
|
|
86
|
+
else controller.enqueue({
|
|
87
|
+
path,
|
|
88
|
+
value: valueToEmit
|
|
89
|
+
});
|
|
90
|
+
} else keep = true;
|
|
91
|
+
else {
|
|
92
|
+
const type = typeof value;
|
|
93
|
+
if (type === "string" || type === "number" || type === "boolean" || value === null) keep = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!keep) {
|
|
97
|
+
for (const row of parserStack) row.value = void 0;
|
|
98
|
+
if (typeof parserValue === "object" && parserValue !== null && parserKey !== void 0) parserValue[parserKey] = void 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
transform(chunk) {
|
|
104
|
+
parser.write(chunk);
|
|
105
|
+
},
|
|
106
|
+
flush(controller) {
|
|
107
|
+
parser.checkEnd();
|
|
108
|
+
controller.terminate();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
this._parser = parser;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
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,8 @@ type Stack = {
|
|
|
10
10
|
value: Value | undefined;
|
|
11
11
|
mode: Mode | undefined;
|
|
12
12
|
}[];
|
|
13
|
-
type OnValue = (value: Value
|
|
14
|
-
declare class
|
|
13
|
+
type OnValue = (value: Value) => void;
|
|
14
|
+
declare class JSONParseStreamRaw {
|
|
15
15
|
tokenizerState: TokenizerState;
|
|
16
16
|
state: Token | ParserState;
|
|
17
17
|
mode: Mode | undefined;
|
|
@@ -44,4 +44,4 @@ declare class JSONParserText {
|
|
|
44
44
|
checkEnd(): void;
|
|
45
45
|
}
|
|
46
46
|
//#endregion
|
|
47
|
-
export {
|
|
47
|
+
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;
|
|
@@ -34,7 +34,6 @@ var JSONParserText = class {
|
|
|
34
34
|
for (let i = 0, l = text.length; i < l; i++) {
|
|
35
35
|
const n = text[i];
|
|
36
36
|
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
37
|
if (this.tokenizerState === "START") if (n === "{") this.onToken("LEFT_BRACE", "{", i);
|
|
39
38
|
else if (n === "}") this.onToken("RIGHT_BRACE", "}", i);
|
|
40
39
|
else if (n === "[") this.onToken("LEFT_BRACKET", "[", i);
|
|
@@ -194,11 +193,8 @@ var JSONParserText = class {
|
|
|
194
193
|
}
|
|
195
194
|
emit(value) {
|
|
196
195
|
if (this.mode) this.state = "COMMA";
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
key: this.key,
|
|
200
|
-
mode: this.mode
|
|
201
|
-
}]);
|
|
196
|
+
if (value === void 0) return;
|
|
197
|
+
this.onValue(value);
|
|
202
198
|
}
|
|
203
199
|
onToken(token, value, i) {
|
|
204
200
|
if (this.stack.length === 0) {
|
|
@@ -255,7 +251,6 @@ var JSONParserText = class {
|
|
|
255
251
|
} else if (!this.seenRootObject) throw new Error("No data in input");
|
|
256
252
|
}
|
|
257
253
|
};
|
|
258
|
-
var JSONParserText_default = JSONParserText;
|
|
259
254
|
|
|
260
255
|
//#endregion
|
|
261
|
-
export {
|
|
256
|
+
export { JSONParseStreamRaw };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { JSONParseStreamRaw } from "./JSONParseStreamRaw.js";
|
|
2
|
+
import { JSONPath } from "./jsonPathToPathArray.js";
|
|
3
|
+
import { JSONParseStream } from "./JSONParseStream.js";
|
|
4
|
+
export { JSONParseStream, JSONParseStreamRaw, type JSONPath };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JSONParseStreamRaw } from "./JSONParseStreamRaw.js";
|
|
2
|
+
import { JSONParseStream } from "./JSONParseStream.js";
|
|
2
3
|
|
|
3
|
-
export {
|
|
4
|
+
export { JSONParseStream, JSONParseStreamRaw };
|
|
@@ -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": "0.0
|
|
3
|
+
"version": "1.0.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",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"lint": "npm-run-all --parallel lint:oxlint lint:tsc",
|
|
27
27
|
"lint:oxlint": "oxlint --type-aware",
|
|
28
28
|
"lint:tsc": "tsc",
|
|
29
|
+
"prepack": "node --run build",
|
|
29
30
|
"prepare": "husky",
|
|
30
31
|
"test": "vitest"
|
|
31
32
|
},
|
|
@@ -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 };
|