-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathjson-stringify.ts
127 lines (115 loc) · 3.78 KB
/
json-stringify.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// deno-lint-ignore-file no-explicit-any no-empty
import { asyncIterToStream } from 'https://ghuc.cc/qwtel/whatwg-stream-to-async-iter/index.ts'
type SeenWeakSet = WeakSet<any>;
type Primitive = undefined | boolean | number | string | bigint | symbol;
export type ToJSON = { toJSON: (key?: any) => string }
export const isIterable = <T>(x: unknown): x is Iterable<T> =>
x != null && typeof x === 'object' && Symbol.iterator in x
export const isAsyncIterable = <T>(x: unknown): x is AsyncIterable<T> =>
x != null && typeof x === 'object' && Symbol.asyncIterator in x
const isPromiseLike = <T>(x: unknown): x is PromiseLike<T> =>
x != null && typeof x === 'object' && 'then' in x && typeof (<any>x).then === 'function'
const isToJSON = <J extends ToJSON>(x: unknown): x is J =>
x != null && typeof x === 'object' && 'toJSON' in x;
const safeAdd = (seen: SeenWeakSet, value: any) => {
if (seen.has(value)) throw TypeError('Converting circular structure to JSON')
seen.add(value)
}
const check = (v: any) => {
if (v === undefined) return false;
const type = typeof v;
return type !== 'function' && type !== 'symbol'
}
// TODO: Add replacer
// TODO: add formatting/spaces
// TODO: concurrent objects/arrays
/**
* @deprecated Change name to something more descriptive!?
*/
export async function* jsonStringifyGenerator(
value: null | Primitive | ToJSON | any[] | Record<string, any> | PromiseLike<any> | AsyncIterable<any> | ReadableStream,
seen: SeenWeakSet = new WeakSet(),
): AsyncIterableIterator<string> {
if (isAsyncIterable(value)) {
yield '['
safeAdd(seen, value)
let first = true;
for await (const v of value) {
if (!first) yield ','; else first = false;
yield* jsonStringifyGenerator(v, seen)
}
seen.delete(value)
yield ']'
}
else if (isPromiseLike(value)) {
const v = await value
if (check(v)) {
safeAdd(seen, value)
yield* jsonStringifyGenerator(v, seen)
seen.delete(value)
}
}
else if (isToJSON(value)) {
const v = JSON.stringify(value);
if (check(v)) yield v
}
else if (Array.isArray(value)) {
yield '['
safeAdd(seen, value)
let first = true;
for (const v of value) {
if (!first) yield ','; else first = false;
yield* jsonStringifyGenerator(v, seen);
}
seen.delete(value)
yield ']'
}
else if (value != null && typeof value === 'object') {
yield '{'
safeAdd(seen, value)
let first = true;
for (const [k, v] of Object.entries(value)) {
if (check(v)) {
const generator = jsonStringifyGenerator(v, seen)
const peek = await generator.next()
if (check(peek.value)) {
if (!first) yield ','; else first = false;
yield `${JSON.stringify(k)}:`
yield peek.value
yield* generator;
}
}
}
seen.delete(value)
yield '}'
}
else {
yield check(value) ? JSON.stringify(value) : 'null'
}
}
/**
* @deprecated Change name to something more descriptive!?
*/
export function jsonStringifyStream(
value: null | Primitive | ToJSON | any[] | Record<string, any> | PromiseLike<any> | AsyncIterable<any> | ReadableStream,
): ReadableStream<string> {
return asyncIterToStream(jsonStringifyGenerator(value))
}
export class JSONStringifyReadable extends ReadableStream<string> {
constructor(value: any) {
let iterator: AsyncIterator<string>;
super({
start() {
iterator = jsonStringifyGenerator(value)[Symbol.asyncIterator]()
},
async pull(controller) {
// console.log('stringify', controller.desiredSize)
const { value, done } = await iterator.next();
if (!done) controller.enqueue(value); else controller.close();
},
async cancel(reason) {
try { await iterator.throw?.(reason) } catch { }
},
})
}
}