Advertisement

← vibesdk

src/utils/ndjson-parser/ndjson-parser.ts

5.6 KB Download file
export async function* ndjsonStream(response: Response) {
    const reader = response.body!.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });

        const lines = buffer.split('\n');
        buffer = lines.pop() ?? "";

        for (const line of lines) {
            if (line.trim()) {
                yield JSON.parse(line);
            }
        }
    }

    // Handle leftover buffer
    if (buffer.trim()) {
        yield JSON.parse(buffer);
    }
}


// Synchronous NDJSON parser for testing
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };

export class NDJSONStreamParser {
    private buffer = '';
    private onMessage: (message: JSONValue) => void;

    constructor(onMessage: (message: JSONValue) => void) {
        this.onMessage = onMessage;
    }

    processChunk(chunk: string): void {
        this.buffer += chunk;
        
        // Handle both \n and \r\n line endings
        const lines = this.buffer.split(/\r?\n/);
        
        // Keep the last partial line in the buffer
        this.buffer = lines.pop() || '';
        
        // Process complete lines
        for (const line of lines) {
            if (line.trim()) {
                try {
                    const message = JSON.parse(line);
                    this.onMessage(message);
                } catch (error) {
                    // Silently ignore invalid JSON
                    console.warn('Invalid JSON in NDJSON stream:', line);
                }
            }
        }
    }
}

export function createRepairingJSONParser() {
    let buffer = '';

    /* util – try JSON.parse without crashing -------------------------- */
    const parse = (str: string) => { try { return JSON.parse(str); } catch { 
        // pass
    } };

    /* STEP-1: close strings when required ----------------------------- */
    function closeStrings(src: string) {
        const out = [];
        let inStr = false, esc = false;

        for (let i = 0; i < src.length; i++) {
            const ch = src[i];

            if (inStr) {
                if (esc) { esc = false; out.push(ch); continue; }
                if (ch === '\\') { esc = true; out.push(ch); continue; }
                if (ch === '"') { inStr = false; out.push(ch); continue; }

                if (ch === ',') {                         // possible string-end
                    let j = i + 1;
                    while (j < src.length && /\s/.test(src[j])) j++;
                    const nxt = src[j] || '';
                    if (nxt === '"' || nxt === '}' || nxt === ']') {
                        out.push('"');                        // ← close the string
                        inStr = false;
                        i--;                                 // re-process the comma
                        continue;
                    }
                }
                out.push(ch);                             // normal char in str
                continue;
            }

            if (ch === '"') { inStr = true; out.push(ch); }
            else out.push(ch);
        }
        if (inStr) out.push('"');                     // EOF inside a string
        return out.join('');
    }

    /* STEP-2: kill dangling commas ------------------------------------ */
    const stripCommas = (s: string) => s.replace(/,\s*([}\]])/g, '$1');

    /* STEP-3: add missing  }  and  ]  --------------------------------- */
    function balanceBrackets(s: string) {
        let inStr = false, esc = false, obj = 0, arr = 0;
        for (const ch of s) {
            if (inStr) {
                if (esc) esc = false;
                else if (ch === '\\') esc = true;
                else if (ch === '"') inStr = false;
                continue;
            }
            if (ch === '"') inStr = true;
            else if (ch === '{') obj++;
            else if (ch === '}') obj--;
            else if (ch === '[') arr++;
            else if (ch === ']') arr--;
        }
        return s + ']'.repeat(Math.max(0, arr)) + '}'.repeat(Math.max(0, obj));
    }

    /* STEP-4: give empty string to dangling keys ---------------------- */
    function fixKeys(s: string) {
        // (a) key right before a }
        s = s.replace(/([{,]\s*)"([^"\\]*(?:\\.[^"\\]*)*)"\s*}(?=,?\s*[\]}]|$)/g,
            (_, pre, k) => `${pre}"${k}": ""}`);

        // (b) key that already has a colon but no value
        s = s.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"\s*:\s*(?=[}\],]|$)/g,
            '"$1": ""');

        // (c) key at very end of the stream
        s = s.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"\s*$/g,
            '"$1": ""');

        return s;
    }

    /* ---------- public API ------------------------------------------ */
    return {
        feed(chunk: string) { buffer += chunk; },

        finalize() {
            let txt = buffer, obj = parse(txt);

            if (obj) return obj;

            txt = closeStrings(txt); 
            obj = parse(txt)
            if (obj) return obj;
            txt = stripCommas(txt); 
            obj = parse(txt)
            if (obj) return obj;
            txt = balanceBrackets(txt);
            obj = parse(txt)
            if (obj) return obj;
            txt = fixKeys(txt); 
            obj = parse(txt)
            if (obj) return obj;

            throw new Error('createRepairingJSONParser: unable to repair JSON');
        }
    };
}