assistant-stream
Version:
Streaming utilities for AI assistants
463 lines (462 loc) • 13.5 kB
JavaScript
import { createJsonParserBackend } from "./json-parser-backend.js";
import { getPartialJsonObjectMeta, parsePartialJsonObject } from "./parse-partial-json-object.js";
// src/utils/json/incremental-json-object-parser.ts
const COMPLETE_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
const PARTIAL_NUMBER = /^-?$|^-?(?:0|[1-9]\d*)(?:\.\d*)?$|^-?(?:0|[1-9]\d*)(?:\.\d+)?[eE][+-]?\d*$/;
const JSON_WHITESPACE = /^[\t\n\r ]$/;
const HEX_DIGIT = /^[0-9a-fA-F]$/;
const SINGLE_ESCAPES = {
"\"": "\"",
"\\": "\\",
"/": "/",
b: "\b",
f: "\f",
n: "\n",
r: "\r",
t: " "
};
const isContainer = (value) => typeof value === "object" && value !== null;
const setValue = (target, key, value) => {
if (key === "__proto__" || key === "constructor" && isContainer(value) && Object.hasOwn(value, "prototype")) throw new Error("Unsafe argument key");
target[key] = value;
};
const cloneContainer = (value) => {
if (Array.isArray(value)) return [...value];
const copy = {};
for (const key of Object.keys(value)) setValue(copy, key, value[key]);
return copy;
};
const writeAtPath = (root, path, value, ownedContainers) => {
const update = (container, depth) => {
const target = ownedContainers.has(container) ? container : cloneContainer(container);
ownedContainers.add(target);
const key = path[depth];
if (depth === path.length - 1) {
setValue(target, key, value);
if (isContainer(value)) ownedContainers.add(value);
return target;
}
const child = target[key];
if (!isContainer(child)) throw new Error("Invalid incremental JSON path");
setValue(target, key, update(child, depth + 1));
return target;
};
return update(root, 0);
};
const createArgsSnapshot = (root, state, partialPath) => {
const result = parsePartialJsonObject("");
for (const key of Object.keys(root)) setValue(result, key, root[key]);
const meta = getPartialJsonObjectMeta(result);
meta.state = state;
meta.partialPath = partialPath.map(String);
return result;
};
const materializeArgsSnapshot = (args) => {
const pending = [args];
while (pending.length > 0) {
const container = pending.pop();
for (const key of Object.keys(container)) {
const value = container[key];
if (!isContainer(value)) continue;
const copy = cloneContainer(value);
setValue(container, key, copy);
pending.push(copy);
}
}
return args;
};
var IncrementalJsonObjectParser = class IncrementalJsonObjectParser {
backend;
text;
mode = "start";
root = {};
frames = [];
token;
args;
publishedArgs;
ownedContainers;
constructor(fallback) {
this.args = fallback;
this.publishedArgs = fallback;
}
static from(text = "", fallback = parsePartialJsonObject("")) {
return IncrementalJsonObjectParser.create(text, fallback, true);
}
static create(text, fallback, allowBackend) {
const parser = new IncrementalJsonObjectParser(fallback);
if (text.length !== 0) parser.text = {
length: text.length,
previous: void 0,
value: text
};
const complete = text.trimEnd().endsWith("}") ? parsePartialJsonObject(text) : void 0;
if (complete && getPartialJsonObjectMeta(complete)?.state === "complete") {
parser.args = complete;
parser.publishedArgs = complete;
parser.mode = "complete";
return parser;
}
if (allowBackend) try {
parser.backend = createJsonParserBackend({
text,
fallback,
createSnapshot: (value, state, partialPath) => materializeArgsSnapshot(createArgsSnapshot(value, state, [...partialPath]))
});
if (parser.backend) {
parser.publishedArgs = parser.backend.currentArgs;
return parser;
}
} catch {
parser.backend = void 0;
}
parser.consumeDelta(text, true);
parser.args = parser.snapshot(fallback);
if (parser.args !== fallback) parser.publishedArgs = parser.mode === "fallback" ? parser.args : void 0;
return parser;
}
get currentTextLength() {
return this.text?.length ?? 0;
}
get currentArgs() {
return this.publishedArgs ??= materializeArgsSnapshot(this.args);
}
append(delta) {
if (delta.length === 0) return this;
if (this.backend) {
let backend;
let args;
try {
backend = this.backend.append(delta);
args = backend?.currentArgs;
} catch {
backend = void 0;
}
if (!backend) return IncrementalJsonObjectParser.create(this.accumulatedText() + delta, this.currentArgs, false);
const parser = new IncrementalJsonObjectParser(this.args);
parser.backend = backend;
parser.publishedArgs = args;
parser.text = {
length: this.currentTextLength + delta.length,
previous: this.text,
value: delta
};
return parser;
}
if (this.currentTextLength === 0) return IncrementalJsonObjectParser.create(delta, this.args, false);
if (this.mode === "complete") {
const parser = IncrementalJsonObjectParser.create(this.accumulatedText() + delta, this.args, false);
if (parser.args === this.args) parser.publishedArgs = this.currentArgs;
return parser;
}
const parser = this.clone();
const fallback = this.args;
parser.consumeDelta(delta);
parser.text = {
length: (this.text?.length ?? 0) + delta.length,
previous: this.text,
value: delta
};
parser.args = parser.snapshot(fallback);
parser.publishedArgs = parser.args === fallback ? this.currentArgs : parser.mode === "fallback" ? parser.args : void 0;
return parser;
}
clone() {
const parser = new IncrementalJsonObjectParser(this.args);
parser.text = this.text;
parser.mode = this.mode;
parser.root = this.root;
parser.frames = this.frames.map((frame) => ({
...frame,
path: [...frame.path]
}));
parser.token = this.token ? {
...this.token,
..."path" in this.token && this.token.path ? { path: [...this.token.path] } : void 0
} : void 0;
return parser;
}
snapshot(fallback) {
if (this.mode === "fallback") return parsePartialJsonObject(this.accumulatedText()) ?? fallback;
if (this.mode === "start") return fallback;
return createArgsSnapshot(this.root, this.mode === "complete" ? "complete" : "partial", this.partialPath());
}
accumulatedText() {
const chunks = [];
for (let chunk = this.text; chunk; chunk = chunk.previous) chunks.push(chunk.value);
return chunks.reverse().join("");
}
partialPath() {
if (this.mode === "complete") return [];
if (this.token && (this.token.kind === "number" || this.token.kind === "string" && this.token.role === "value")) return this.token.path;
return this.frames.at(-1)?.path ?? [];
}
isFallback() {
return this.mode === "fallback";
}
consumeDelta(delta, ownsRoot = false) {
if (this.isFallback()) return;
this.ownedContainers = ownsRoot ? /* @__PURE__ */ new Set([this.root]) : /* @__PURE__ */ new Set();
for (const char of delta) {
this.consumeCharacter(char);
if (this.isFallback()) break;
}
if (!this.isFallback() && this.token?.kind === "string" && this.token.role === "value") this.writeValue(this.token.path, this.token.value);
else if (!this.isFallback() && this.token?.kind === "number" && COMPLETE_NUMBER.test(this.token.value)) this.writeValue(this.token.path, Number(this.token.value));
this.ownedContainers = void 0;
}
consumeCharacter(char) {
let reconsume = true;
while (reconsume && this.mode !== "fallback") {
reconsume = false;
if (this.token) {
reconsume = this.consumeToken(char);
continue;
}
if (this.mode === "complete") {
if (!JSON_WHITESPACE.test(char)) this.mode = "fallback";
continue;
}
if (this.mode === "start") {
if (JSON_WHITESPACE.test(char)) continue;
if (char !== "{") {
this.mode = "fallback";
continue;
}
this.mode = "parsing";
this.frames.push({
kind: "object",
path: [],
state: "first-key-or-end"
});
continue;
}
const frame = this.frames.at(-1);
if (!frame) {
this.mode = "fallback";
continue;
}
if (frame.kind === "object") this.consumeObjectCharacter(frame, char);
else this.consumeArrayCharacter(frame, char);
}
}
consumeObjectCharacter(frame, char) {
if (JSON_WHITESPACE.test(char)) return;
switch (frame.state) {
case "first-key-or-end":
if (char === "}") this.closeContainer();
else if (char === "\"") {
frame.state = "key";
this.startKeyString();
} else this.mode = "fallback";
break;
case "key":
if (char === "\"") this.startKeyString();
else this.mode = "fallback";
break;
case "colon":
if (char === ":") frame.state = "value";
else this.mode = "fallback";
break;
case "value":
this.startValue(char, [...frame.path, frame.key]);
break;
case "comma-or-end": if (char === ",") {
frame.state = "key";
frame.key = void 0;
} else if (char === "}") this.closeContainer();
else this.mode = "fallback";
}
}
consumeArrayCharacter(frame, char) {
if (JSON_WHITESPACE.test(char)) return;
switch (frame.state) {
case "first-value-or-end":
if (char === "]") this.closeContainer();
else {
frame.state = "value";
this.startValue(char, [...frame.path, frame.index]);
}
break;
case "value":
this.startValue(char, [...frame.path, frame.index]);
break;
case "comma-or-end": if (char === ",") {
frame.index += 1;
frame.state = "value";
} else if (char === "]") this.closeContainer();
else this.mode = "fallback";
}
}
startValue(char, path) {
if (char === "{") {
this.writeValue(path, {});
this.frames.push({
kind: "object",
path,
state: "first-key-or-end"
});
} else if (char === "[") {
this.writeValue(path, []);
this.frames.push({
kind: "array",
path,
state: "first-value-or-end",
index: 0
});
} else if (char === "\"") {
this.writeValue(path, "");
this.startValueString(path);
} else if (char === "t" || char === "f" || char === "n") {
const value = char === "t" ? "true" : char === "f" ? "false" : "null";
this.writeValue(path, value === "true" ? true : value === "false" ? false : null);
this.token = {
kind: "literal",
value,
offset: 1
};
} else if (char === "-" || char >= "0" && char <= "9") {
this.token = {
kind: "number",
path,
value: char
};
if (COMPLETE_NUMBER.test(char)) this.writeValue(path, Number(char));
} else this.mode = "fallback";
}
startKeyString() {
this.token = {
kind: "string",
role: "key",
value: "",
escape: "none",
unicode: ""
};
}
startValueString(path) {
this.token = {
kind: "string",
role: "value",
path,
value: "",
escape: "none",
unicode: ""
};
}
consumeToken(char) {
const token = this.token;
if (token.kind === "string") return this.consumeString(token, char);
if (token.kind === "number") return this.consumeNumber(token, char);
return this.consumeLiteral(token, char);
}
consumeString(token, char) {
if (token.escape === "unicode") {
if (!HEX_DIGIT.test(char)) {
this.mode = "fallback";
return false;
}
token.unicode += char;
if (token.unicode.length === 4) {
token.value += String.fromCharCode(Number.parseInt(token.unicode, 16));
token.escape = "none";
token.unicode = "";
}
return false;
}
if (token.escape === "single") {
if (char === "u") {
token.escape = "unicode";
return false;
}
const decoded = SINGLE_ESCAPES[char];
if (decoded === void 0) this.mode = "fallback";
else {
token.value += decoded;
token.escape = "none";
}
return false;
}
if (char === "\\") {
token.escape = "single";
return false;
}
if (char.charCodeAt(0) < 32) {
this.mode = "fallback";
return false;
}
if (char !== "\"") {
token.value += char;
return false;
}
this.token = void 0;
if (token.role === "key") {
const frame = this.frames.at(-1);
if (!frame || frame.kind !== "object") {
this.mode = "fallback";
return false;
}
if (token.value === "__proto__" || token.value === "prototype" && frame.path.at(-1) === "constructor") {
this.mode = "fallback";
return false;
}
frame.key = token.value;
frame.state = "colon";
} else {
this.writeValue(token.path, token.value);
this.finishValue();
}
return false;
}
consumeNumber(token, char) {
if (char >= "0" && char <= "9" || char === "e" || char === "E" || char === "+" || char === "-" || char === ".") {
const value = token.value + char;
if (!PARTIAL_NUMBER.test(value)) {
this.mode = "fallback";
return false;
}
token.value = value;
if (COMPLETE_NUMBER.test(token.value)) this.writeValue(token.path, Number(token.value));
return false;
}
if (!COMPLETE_NUMBER.test(token.value)) {
this.mode = "fallback";
return false;
}
this.token = void 0;
this.finishValue();
return true;
}
consumeLiteral(token, char) {
if (char !== token.value[token.offset]) {
this.mode = "fallback";
return false;
}
token.offset += 1;
if (token.offset === token.value.length) {
this.token = void 0;
this.finishValue();
}
return false;
}
finishValue() {
const frame = this.frames.at(-1);
if (!frame || frame.state !== "value") {
this.mode = "fallback";
return;
}
frame.state = "comma-or-end";
}
closeContainer() {
this.frames.pop();
if (this.frames.length === 0) this.mode = "complete";
else this.finishValue();
}
writeValue(path, value) {
try {
this.root = writeAtPath(this.root, path, value, this.ownedContainers ?? /* @__PURE__ */ new Set());
} catch {
this.mode = "fallback";
}
}
};
export { IncrementalJsonObjectParser };
//# sourceMappingURL=incremental-json-object-parser.js.map