@worker-tools/json-stream
Version:
Utilities for working with streaming JSON in Worker Runtimes such as Cloudflare Workers, Deno Deploy and Service Workers.
123 lines • 3.96 kB
JavaScript
// deno-lint-ignore-file no-explicit-any no-empty
import { asyncIterToStream } from 'whatwg-stream-to-async-iter';
export const isIterable = (x) => x != null && typeof x === 'object' && Symbol.iterator in x;
export const isAsyncIterable = (x) => x != null && typeof x === 'object' && Symbol.asyncIterator in x;
const isPromiseLike = (x) => x != null && typeof x === 'object' && 'then' in x && typeof x.then === 'function';
const isToJSON = (x) => x != null && typeof x === 'object' && 'toJSON' in x;
const safeAdd = (seen, value) => {
if (seen.has(value))
throw TypeError('Converting circular structure to JSON');
seen.add(value);
};
const check = (v) => {
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, seen = new WeakSet()) {
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) {
return asyncIterToStream(jsonStringifyGenerator(value));
}
export class JSONStringifyReadable extends ReadableStream {
constructor(value) {
let iterator;
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) {
var _a;
try {
await ((_a = iterator.throw) === null || _a === void 0 ? void 0 : _a.call(iterator, reason));
}
catch { }
},
});
}
}
//# sourceMappingURL=json-stringify.js.map