← Files DataARCHIVED FILE
templates/data-app/base/src/streaming-json.js
14.9 KB · Sep 30, 2026 · 23:19 UTC
// Portable JSON parsing/serialization. Chunks bound temporary strings; callers
// still own the complete parsed value unless they explicitly discard containers.
const CHUNK_SIZE = 64 * 1024;
const NUMBER_DIGITS = 1100;
const JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
const STRING_SPECIAL = /["\\\u0000-\u001f]/g;
const ESCAPES = { '"': '"', "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: "\t" };
/**
* Incrementally parse exactly one JSON value. write accepts strings (not bytes).
* discard(path) may replace a container with an empty {} or [], while its
* complete grammar is still checked. Paths use string keys and numeric indices.
* onValue({ value, path }) observes completed values, including overwritten
* duplicate properties; discarded containers are represented by placeholders.
*/
export function createStreamingJsonParser({ discard, onValue } = {}) {
const stack = [];
let root;
let rootComplete = false;
let finished = false;
let failed = false;
let offset = 0;
let token = null;
function invalid(message = "Invalid JSON") {
failed = true;
throw new SyntaxError(`${message} at position ${offset}`);
}
function pathForValue() {
const path = stack.slice(1).map((frame) => frame.segment);
const parent = stack.at(-1);
if (parent) path.push(parent.type === "array" ? parent.index : parent.key);
return path;
}
function complete(value) {
if (onValue) onValue({ value, path: pathForValue() });
const parent = stack.at(-1);
if (!parent) {
root = value;
rootComplete = true;
} else if (parent.type === "array") {
if (!parent.skip) parent.value.push(value);
parent.index += 1;
parent.phase = "comma";
} else {
if (!parent.skip) {
// Assignment to this name would invoke Object.prototype's setter.
if (parent.key === "__proto__") {
Object.defineProperty(parent.value, parent.key, {
value, writable: true, enumerable: true, configurable: true,
});
} else parent.value[parent.key] = value;
}
parent.key = null;
parent.phase = "comma";
}
}
function closeContainer() {
const frame = stack.pop();
complete(frame.value);
}
function addDigit(character) {
if (token.fraction) token.fractionDigits += 1;
if (token.significantDigits === 0 && character === "0") return;
token.significantDigits += 1;
if (token.digits.length < NUMBER_DIGITS) token.digits += character;
else if (character !== "0") token.sticky = true;
}
function finishNumber() {
if (!["zero", "integer", "fraction", "exponent"].includes(token.phase)) invalid("Incomplete JSON number");
// Every binary64 rounding boundary has fewer than 1100 significant decimal
// digits. Keep that prefix plus one sticky digit so arbitrarily long JSON
// numbers round exactly, including subnormals and overflow, without keeping
// an arbitrarily long token string.
let digits = token.digits;
if (token.sticky) digits += "1";
const exponent = token.exponentSign * token.exponent - token.fractionDigits
+ token.significantDigits - digits.length;
const value = digits.length === 0 ? (token.negative ? -0 : 0)
: Number(`${token.negative ? "-" : ""}${digits}e${exponent}`);
token = null;
complete(value);
}
function numberCharacter(character) {
const digit = character >= "0" && character <= "9";
if (token.phase === "sign") {
if (!digit) invalid("Invalid JSON number");
token.phase = character === "0" ? "zero" : "integer";
addDigit(character);
} else if (token.phase === "integer" && digit) addDigit(character);
else if ((token.phase === "zero" || token.phase === "integer") && character === ".") {
token.fraction = true;
token.phase = "dot";
} else if (token.phase === "dot" && digit) {
token.phase = "fraction";
addDigit(character);
} else if (token.phase === "fraction" && digit) addDigit(character);
else if (["zero", "integer", "fraction"].includes(token.phase) && (character === "e" || character === "E")) token.phase = "e";
else if (token.phase === "e" && (character === "+" || character === "-")) {
token.exponentSign = character === "-" ? -1 : 1;
token.phase = "exponent-sign";
} else if (["e", "exponent-sign", "exponent"].includes(token.phase) && digit) {
token.phase = "exponent";
token.exponent = Math.min(Number.MAX_SAFE_INTEGER, token.exponent * 10 + Number(character));
} else return false;
return true;
}
function startString(key) {
token = { type: "string", key, parts: [], escape: false, unicode: null,
keep: Boolean(onValue || !stack.at(-1)?.skip) };
}
function startValue(character) {
if (character === "{" || character === "[") {
const parent = stack.at(-1);
const segment = parent ? (parent.type === "array" ? parent.index : parent.key) : null;
const skip = Boolean(parent?.skip || (discard && discard(pathForValue())));
const type = character === "[" ? "array" : "object";
stack.push({ type, value: type === "array" ? [] : {}, phase: "first", skip, segment, key: null, index: 0 });
} else if (character === '"') startString(false);
else if (character === "t" || character === "f" || character === "n") {
token = { type: "literal", expected: character === "t" ? "true" : character === "f" ? "false" : "null", index: 1 };
} else if (character === "-" || (character >= "0" && character <= "9")) {
token = { type: "number", phase: "sign", negative: character === "-", digits: "", significantDigits: 0,
fraction: false, fractionDigits: 0, exponent: 0, exponentSign: 1, sticky: false };
if (character !== "-") numberCharacter(character);
} else invalid("Expected a JSON value");
}
function write(text) {
if (finished || failed) throw new SyntaxError("JSON parser is no longer writable");
if (typeof text !== "string") throw new TypeError("JSON parser chunks must be strings");
let index = 0;
let stringSpecialIndex = -1;
try {
while (index < text.length) {
const character = text[index];
if (token?.type === "string") {
if (token.unicode !== null) {
if (!/[0-9a-fA-F]/.test(character)) invalid("Invalid JSON Unicode escape");
token.unicode += character;
if (token.unicode.length === 4) {
if (token.keep) token.parts.push(String.fromCharCode(Number.parseInt(token.unicode, 16)));
token.unicode = null;
}
} else if (token.escape) {
token.escape = false;
if (character === "u") token.unicode = "";
else if (Object.hasOwn(ESCAPES, character)) {
if (token.keep) token.parts.push(ESCAPES[character]);
}
else invalid("Invalid JSON escape");
} else if (character === '"') {
const value = token.parts.join("");
const key = token.key;
token = null;
if (key) {
stack.at(-1).key = value;
stack.at(-1).phase = "colon";
} else complete(value);
} else if (character === "\\") token.escape = true;
else if (character < " ") invalid("Unescaped control character in JSON string");
else {
if (stringSpecialIndex < index) {
STRING_SPECIAL.lastIndex = index;
stringSpecialIndex = STRING_SPECIAL.exec(text)?.index ?? text.length;
}
const end = Math.min(stringSpecialIndex, index + CHUNK_SIZE);
// Native parsing copies this bounded, escape-free span. Keeping a
// substring instead can retain its entire decoded input window for
// the lifetime of a tiny cell value (notably in V8).
if (token.keep) token.parts.push(JSON.parse(`"${text.slice(index, end)}"`));
offset += end - index;
index = end;
continue;
}
} else if (token?.type === "number") {
if (!numberCharacter(character)) {
finishNumber();
continue;
}
} else if (token?.type === "literal") {
if (character !== token.expected[token.index]) invalid("Invalid JSON literal");
token.index += 1;
if (token.index === token.expected.length) {
const value = token.expected === "true" ? true : token.expected === "false" ? false : null;
token = null;
complete(value);
}
} else if (!JSON_WHITESPACE.has(character)) {
const frame = stack.at(-1);
if (!frame) {
if (rootComplete) invalid("Unexpected content after JSON value");
startValue(character);
} else if (frame.type === "array") {
if (frame.phase === "comma") {
if (character === ",") frame.phase = "value";
else if (character === "]") closeContainer();
else invalid("Expected ',' or ']' in JSON array");
} else if (frame.phase === "first" && character === "]") closeContainer();
else startValue(character);
} else if (frame.phase === "first" && character === "}") closeContainer();
else if (frame.phase === "first" || frame.phase === "key") {
if (character !== '"') invalid("Expected a JSON object key");
startString(true);
} else if (frame.phase === "colon") {
if (character !== ":") invalid("Expected ':' after JSON object key");
frame.phase = "value";
} else if (frame.phase === "value") startValue(character);
else if (character === ",") frame.phase = "key";
else if (character === "}") closeContainer();
else invalid("Expected ',' or '}' in JSON object");
}
index += 1;
offset += 1;
}
} catch (error) {
failed = true;
throw error;
}
}
function finish() {
if (finished || failed) throw new SyntaxError("JSON parser has already finished or failed");
finished = true;
if (token?.type === "number") finishNumber();
if (token || stack.length || !rootComplete) invalid("Unexpected end of JSON input");
return root;
}
return { write, finish };
}
/** Decode bounded byte slices, matching Buffer UTF-8 decoding by default. */
export function parseJsonBytes(bytes, { fatal = false, ignoreBOM = true, ...options } = {}) {
const view = bytes instanceof ArrayBuffer ? new Uint8Array(bytes)
: new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const decoder = new TextDecoder("utf-8", { fatal, ignoreBOM });
const parser = createStreamingJsonParser(options);
for (let offset = 0; offset < view.byteLength; offset += CHUNK_SIZE) {
parser.write(decoder.decode(view.subarray(offset, offset + CHUNK_SIZE), { stream: true }));
}
parser.write(decoder.decode());
return parser.finish();
}
/** Match Response.json UTF-8/BOM behavior without a whole-response string. */
export async function parseJsonResponse(response, { fatal = false, ignoreBOM = false, ...options } = {}) {
const parser = createStreamingJsonParser(options);
const decoder = new TextDecoder("utf-8", { fatal, ignoreBOM });
if (!response.body) return parser.finish();
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (let offset = 0; offset < value.byteLength; offset += CHUNK_SIZE) {
parser.write(decoder.decode(value.subarray(offset, offset + CHUNK_SIZE), { stream: true }));
}
}
parser.write(decoder.decode());
return parser.finish();
} catch (error) {
try { await reader.cancel(error); } catch { /* Preserve the parse/read error. */ }
throw error;
} finally {
reader.releaseLock();
}
}
function* stringChunks(value) {
yield '"';
for (let offset = 0; offset < value.length;) {
let end = Math.min(offset + CHUNK_SIZE / 8, value.length);
// JSON.stringify escapes lone surrogates; never split a valid pair.
if (end < value.length && value.charCodeAt(end - 1) >= 0xd800 && value.charCodeAt(end - 1) <= 0xdbff
&& value.charCodeAt(end) >= 0xdc00 && value.charCodeAt(end) <= 0xdfff) end -= 1;
yield JSON.stringify(value.slice(offset, end)).slice(1, -1);
offset = end;
}
yield '"';
}
function jsonValue(holder, key) {
let value = holder[key];
if ((value !== null && typeof value === "object") || typeof value === "function" || typeof value === "bigint") {
const toJSON = value.toJSON;
if (typeof toJSON === "function") value = toJSON.call(value, key);
}
if (value instanceof Number) value = +value;
else if (value instanceof String) value = String(value);
else if (value instanceof Boolean) value = Boolean.prototype.valueOf.call(value);
else if (value instanceof BigInt) value = BigInt.prototype.valueOf.call(value);
return value;
}
/** Yield compact JSON.stringify-compatible output; undefined roots emit nothing. */
export function* stringifyJsonChunks(value) {
const ancestors = new Set();
const stack = [{ type: "value", value: jsonValue({ "": value }, "") }];
let buffer = "";
function* emit(text) {
if (buffer.length + text.length > CHUNK_SIZE) {
if (buffer) yield buffer;
buffer = "";
}
buffer += text;
}
while (stack.length) {
const frame = stack.at(-1);
if (frame.type === "value") {
stack.pop();
const current = frame.value;
if (typeof current === "string") {
for (const part of stringChunks(current)) yield* emit(part);
} else if (current === null || typeof current === "number" || typeof current === "boolean") yield* emit(JSON.stringify(current));
else if (typeof current === "bigint") throw new TypeError("Do not know how to serialize a BigInt");
else if (typeof current === "object") {
if (ancestors.has(current)) throw new TypeError("Converting circular structure to JSON");
ancestors.add(current);
const array = Array.isArray(current);
stack.push({ type: array ? "array" : "object", value: current, keys: array ? null : Object.keys(current),
length: array ? current.length : 0, index: 0, emitted: false });
yield* emit(array ? "[" : "{");
}
} else {
if (frame.index === (frame.keys?.length ?? frame.length)) {
stack.pop();
ancestors.delete(frame.value);
yield* emit(frame.type === "array" ? "]" : "}");
continue;
}
const key = frame.keys ? frame.keys[frame.index] : String(frame.index);
frame.index += 1;
let current = jsonValue(frame.value, key);
if (current === undefined || typeof current === "function" || typeof current === "symbol") {
if (frame.type === "object") continue;
current = null;
}
if (frame.emitted) yield* emit(",");
frame.emitted = true;
if (frame.type === "object") {
for (const part of stringChunks(key)) yield* emit(part);
yield* emit(":");
}
stack.push({ type: "value", value: current });
}
}
if (buffer) yield buffer;
}
SHA-256: 5fa4ab801f008c755d1d956034b82af9df8ad0ffe8d0178a67d006fa5573a7ce