Skip to content

Commit 3d16a7a

Browse files
feat(a-json): add new async json package
1 parent a83cd27 commit 3d16a7a

File tree

8 files changed

+365
-0
lines changed

8 files changed

+365
-0
lines changed

.cz-config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ module.exports = {
4343
{ name: 'action-chain' },
4444
{ name: 'proxy-state-tree' },
4545
{ name: 'betsy' },
46+
{ name: 'a-json' },
4647
],
4748

4849
// it needs to match the value for field type. Eg.: 'fix'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
src
2+
jest.config.js
3+
rollup.config.js
4+
tsconfig.json
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# a-json
2+
3+
Async `JSON` parse and stringify
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module.exports = {
2+
collectCoverage: true,
3+
collectCoverageFrom: ['src/**/*.{t,j}s?(x)', '!src/**/*.d.ts'],
4+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
5+
transform: {
6+
'^.+\\.tsx?$': 'ts-jest',
7+
},
8+
testRegex: '\\.test\\.tsx?$',
9+
testPathIgnorePatterns: [
10+
'/dist/',
11+
'/es/',
12+
'/lib/',
13+
'<rootDir>/node_modules/',
14+
],
15+
transformIgnorePatterns: ['<rootDir>/node_modules/'],
16+
coveragePathIgnorePatterns: ['<rootDir>/node_modules/'],
17+
haste: {
18+
// This option is needed or else globbing ignores <rootDir>/node_modules.
19+
providesModuleNodeModules: ['a-json'],
20+
},
21+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "a-json",
3+
"version": "1.0.0",
4+
"description": "Async JSON parse and stringify",
5+
"author": "Christian Alfoni <[email protected]>",
6+
"license": "MIT",
7+
"repository": "git+https://github.com/cerebral/overmind.git",
8+
"main": "lib/index.js",
9+
"module": "es/index.js",
10+
"types": "lib/index.d.ts",
11+
"scripts": {
12+
"build": "npm run build:lib & npm run build:es",
13+
"build:lib": "tsc --outDir lib --module commonjs",
14+
"build:es": "tsc --outDir es --module es2015",
15+
"clean": "rimraf es lib coverage",
16+
"typecheck": "tsc --noEmit",
17+
"test": "jest",
18+
"test:watch": "jest --watch --updateSnapshot --coverage false",
19+
"prebuild": "npm run clean",
20+
"postbuild": "rimraf {lib,es}/**/__tests__",
21+
"posttest": "npm run typecheck"
22+
},
23+
"keywords": [
24+
"events",
25+
"eventemitter",
26+
"eventhub"
27+
],
28+
"files": [
29+
"lib",
30+
"es"
31+
],
32+
"devDependencies": {
33+
"@types/node": "^12.11.6",
34+
"tslib": "^1.10.0"
35+
}
36+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { stringify, parse } from ".";
2+
3+
test("should stringify objects", async () => {
4+
const obj = {
5+
foo: "bar",
6+
bar: {
7+
isAwesome: true
8+
},
9+
baz: ["foo", 123, true]
10+
};
11+
const result = await stringify(obj);
12+
expect(result).toBe(JSON.stringify(obj));
13+
});
14+
15+
test("should parse string", async () => {
16+
const obj = JSON.stringify({
17+
foo: "bar",
18+
bar: {
19+
isAwesome: true
20+
},
21+
baz: ["foo", false, 123]
22+
});
23+
const result = await parse(obj);
24+
expect(result).toEqual(JSON.parse(obj));
25+
});
26+
27+
test("should stringify objects in arrays", async () => {
28+
const obj = {
29+
demos: [
30+
{
31+
title: "Simple app"
32+
}
33+
]
34+
};
35+
const result = await stringify(obj);
36+
expect(result).toBe(JSON.stringify(obj));
37+
});
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// Symbol used as placeholders for objects and arrays
2+
const IS_ARRAY = Symbol("IS_ARRAY");
3+
const IS_OBJECT = Symbol("IS_OBJECT");
4+
5+
let ASYNC_BATCH_COUNT = 3000;
6+
let TRACK_PERF = false;
7+
8+
export const tweak = (batchCount, trackPerf) => {
9+
ASYNC_BATCH_COUNT = batchCount;
10+
TRACK_PERF = Boolean(trackPerf);
11+
};
12+
13+
const next =
14+
(typeof window !== "undefined" && window.requestAnimationFrame) || setTimeout;
15+
16+
/*
17+
This function takes a value which results in a Map of
18+
path to a value and placeholder IS_ARRAY/IS_OBJECT, or the static
19+
value. It takes a "replacer" function which allows you to intercept
20+
the created result
21+
*/
22+
function getPaths(value, paths, replacer, currentPath = []) {
23+
const replacedValue = replacer ? replacer(currentPath, value) : value;
24+
if (
25+
typeof replacedValue === "function" ||
26+
typeof replacedValue === "symbol"
27+
) {
28+
} else if (Array.isArray(replacedValue)) {
29+
paths.set(currentPath.slice(), IS_ARRAY);
30+
for (let key in replacedValue) {
31+
currentPath.push(key);
32+
getPaths(replacedValue[key], paths, replacer, currentPath);
33+
currentPath.pop();
34+
}
35+
} else if (replacedValue !== null && typeof replacedValue === "object") {
36+
paths.set(currentPath.slice(), IS_OBJECT);
37+
for (let key in replacedValue) {
38+
currentPath.push(key);
39+
getPaths(replacedValue[key], paths, replacer, currentPath);
40+
currentPath.pop();
41+
}
42+
} else {
43+
paths.set(currentPath.slice(), replacedValue);
44+
}
45+
46+
return paths;
47+
}
48+
49+
/*
50+
When parsing we replace all objects and arrays with a reference first,
51+
then we async resolve them. This function identifies a ref
52+
*/
53+
function isRef(ref) {
54+
if (typeof ref === "string" && ref.match(/\$\$REF_[0-9]/)) {
55+
return ref;
56+
}
57+
58+
return null;
59+
}
60+
61+
/*
62+
This function takes value with an optional replacer. It will stringify
63+
the values async by first creating a MAP of all paths, as explained above.
64+
Then it iterates these paths and produces the JSON string
65+
*/
66+
export function stringify(value, replacer?) {
67+
return new Promise(resolve => {
68+
next(async () => {
69+
const paths = getPaths(value, new Map(), replacer);
70+
const structs = [];
71+
let prevKey = [];
72+
let string = "";
73+
let x = 0;
74+
let start;
75+
if (TRACK_PERF) {
76+
start = performance.now();
77+
}
78+
for (let [key, value] of paths) {
79+
if (x % ASYNC_BATCH_COUNT === 0) {
80+
if (TRACK_PERF) {
81+
const end = performance.now();
82+
console.log("PERF stringify", end - start);
83+
start = end;
84+
}
85+
86+
await new Promise(resolve => next(resolve));
87+
}
88+
89+
x++;
90+
91+
const currentKey = prevKey;
92+
93+
prevKey = key;
94+
95+
if (
96+
// If we are moving back up the tree, previous has to have been
97+
// an object or array
98+
(key.length && currentKey.length > key.length) ||
99+
// Or we stay at the same nested level, but previous was
100+
// an object or key
101+
(currentKey.length === key.length &&
102+
(paths.get(currentKey) === IS_OBJECT ||
103+
paths.get(currentKey) === IS_ARRAY))
104+
) {
105+
string += structs.pop() === IS_OBJECT ? "}" : "]";
106+
}
107+
108+
if (key.length && key.length <= currentKey.length) {
109+
string += ",";
110+
}
111+
112+
if (value === IS_OBJECT) {
113+
const currentStruct = structs[structs.length - 1];
114+
structs.push(value);
115+
string +=
116+
key.length && currentStruct === IS_OBJECT
117+
? `"${key[key.length - 1]}":{`
118+
: "{";
119+
continue;
120+
}
121+
122+
if (value === IS_ARRAY) {
123+
const currentStruct = structs[structs.length - 1];
124+
structs.push(value);
125+
string +=
126+
key.length && currentStruct === IS_OBJECT
127+
? `"${key[key.length - 1]}":[`
128+
: "[";
129+
continue;
130+
}
131+
132+
const currentStruct = structs[structs.length - 1];
133+
if (currentStruct === IS_OBJECT) {
134+
string += `"${key[key.length - 1]}":${JSON.stringify(value)}`;
135+
continue;
136+
}
137+
if (currentStruct === IS_ARRAY) {
138+
string += JSON.stringify(value);
139+
continue;
140+
}
141+
}
142+
143+
while (structs.length) {
144+
const struct = structs.pop();
145+
string += struct === IS_OBJECT ? "}" : "]";
146+
}
147+
148+
resolve(string);
149+
});
150+
});
151+
}
152+
153+
/*
154+
This function takes a JSON string and async parses it. It does this by looking
155+
tracking all "{", "[" and then when finding their end parts, "}" and "]", replaces
156+
the object/array with a reference. Each reference is then async parsed and
157+
built back together
158+
*/
159+
export async function parse(value) {
160+
let refId = 0;
161+
const references = {};
162+
const openingObjectBrackets = [];
163+
const openingArrayBrackets = [];
164+
165+
let isInString = false;
166+
let start;
167+
if (TRACK_PERF) {
168+
start = performance.now();
169+
}
170+
for (let charIndex = 0; charIndex < value.length; charIndex++) {
171+
if (charIndex % ASYNC_BATCH_COUNT === 0) {
172+
if (TRACK_PERF) {
173+
const end = performance.now();
174+
console.log("PERF parse", end - start);
175+
start = end;
176+
}
177+
await new Promise(resolve => next(resolve));
178+
}
179+
if (value[charIndex] === '"') {
180+
isInString = !isInString;
181+
continue;
182+
}
183+
184+
if (isInString) {
185+
continue;
186+
}
187+
188+
if (value[charIndex] === "{") {
189+
openingObjectBrackets.push(charIndex);
190+
} else if (value[charIndex] === "[") {
191+
openingArrayBrackets.push(charIndex);
192+
} else if (value[charIndex] === "}") {
193+
const openingBracketIndex = openingObjectBrackets.pop();
194+
const id = `$$REF_${refId++}`;
195+
references[id] = value.substr(
196+
openingBracketIndex,
197+
charIndex - openingBracketIndex + 1
198+
);
199+
value =
200+
value.substr(0, openingBracketIndex) +
201+
`"${id}"` +
202+
value.substr(charIndex + 1);
203+
204+
charIndex = openingBracketIndex + id.length + 1;
205+
} else if (value[charIndex] === "]") {
206+
const openingBracketIndex = openingArrayBrackets.pop();
207+
const id = `$$REF_${refId++}`;
208+
references[id] = value.substr(
209+
openingBracketIndex,
210+
charIndex - openingBracketIndex + 1
211+
);
212+
value =
213+
value.substr(0, openingBracketIndex) +
214+
`"${id}"` +
215+
value.substr(charIndex + 1);
216+
charIndex = openingBracketIndex + id.length + 1;
217+
}
218+
}
219+
220+
async function produceResult(value, i) {
221+
let parsedValue = JSON.parse(value);
222+
223+
const keys = Object.keys(parsedValue);
224+
225+
if (i % ASYNC_BATCH_COUNT === 0) {
226+
await new Promise(resolve => next(resolve));
227+
}
228+
229+
return Promise.all(
230+
keys.map(async (key, index) => {
231+
const value = parsedValue[key];
232+
i++;
233+
if (isRef(value)) {
234+
parsedValue[key] = await produceResult(references[value], i + index);
235+
}
236+
})
237+
).then(() => parsedValue);
238+
}
239+
240+
let initialValue = JSON.parse(value);
241+
if (isRef(initialValue)) {
242+
initialValue = references[initialValue];
243+
}
244+
245+
return produceResult(initialValue, 0);
246+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es6",
4+
"module": "commonjs",
5+
"rootDir": "src",
6+
"outDir": ".code",
7+
"newLine": "LF",
8+
"lib": ["dom", "es2017"],
9+
"moduleResolution": "node",
10+
"importHelpers": true,
11+
"declaration": true,
12+
"pretty": true,
13+
"sourceMap": true,
14+
"inlineSources": true
15+
},
16+
"exclude": ["node_modules", "dist", "es", "lib", "src/**/*.test.ts"]
17+
}

0 commit comments

Comments
 (0)