All files / json-pack/src/json JsonDecoderPartial.ts

86.95% Statements 60/69
71.42% Branches 15/21
100% Functions 4/4
92.72% Lines 51/55

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 1042x     2x 24x 24x                                                   2x   7860x 7860x   31x 7x         1857x 1857x 1857x 1857x 1857x 1857x 3495x 3495x 3495x 1649x 1176x 1642x 1642x 1642x   4x 4x     1638x         2739x 2739x 2739x 2739x 2739x 12218x 12218x 12218x 9499x 3434x 3434x   6065x 6065x 6065x 6052x 6052x 6052x 6052x 6048x 6048x 6048x   3x       3x     20x 20x            
import {JsonDecoder, readKey} from './JsonDecoder';
import type {PackValue} from '../types';
 
export class DecodeFinishError extends Error {
  constructor(public readonly value: unknown) {
    super('DECODE_FINISH');
  }
}
 
/**
 * This class parses JSON which is mostly correct but not necessarily complete
 * or with missing parts. It can be used to parse JSON that is being streamed
 * in chunks or JSON output of an LLM model.
 *
 * If the end of a nested JSON value (array, object) is missing, this parser
 * will return the initial correct part for that value, which it was able to
 * parse, until the point where the JSON is no longer valid.
 *
 * Examples:
 *
 * ```js
 * // Missing closing brace
 * decoder.readAny('[1, 2, 3'); // [1, 2, 3]
 *
 * // Trailing comma and missing closing brace
 * decoder.readAny('[1, 2, '); // [1, 2]
 *
 * // Corrupt second element and missing closing brace
 * decoder.readAny('{"foo": 1, "bar":'); // {"foo": 1}
 * ```
 */
export class JsonDecoderPartial extends JsonDecoder {
  public readAny(): unknown {
    try {
      return super.readAny();
    } catch (error) {
      if (error instanceof DecodeFinishError) return error.value;
      throw error;
    }
  }
 
  public readArr(): unknown[] {
    const reader = this.reader;
    Iif (reader.u8() !== 0x5b /* [ */) throw new Error('Invalid JSON');
    const arr: unknown[] = [];
    const uint8 = reader.uint8;
    let first = true;
    while (true) {
      this.skipWhitespace();
      const char = uint8[reader.x];
      if (char === 0x5d /* ] */) return reader.x++, arr;
      if (char === 0x2c /* , */) reader.x++;
      else if (!first) return arr;
      this.skipWhitespace();
      try {
        arr.push(this.readAny());
      } catch (error) {
        Iif (error instanceof DecodeFinishError) return arr.push(error.value), arr;
        if (error instanceof Error && error.message === 'Invalid JSON') throw new DecodeFinishError(arr);
        throw error;
      }
      first = false;
    }
  }
 
  public readObj(): PackValue | Record<string, unknown> | unknown {
    const reader = this.reader;
    Iif (reader.u8() !== 0x7b /* { */) throw new Error('Invalid JSON');
    const obj: Record<string, unknown> = {};
    const uint8 = reader.uint8;
    while (true) {
      this.skipWhitespace();
      let char = uint8[reader.x];
      if (char === 0x7d /* } */) return reader.x++, obj;
      if (char === 0x2c /* , */) {
        reader.x++;
        continue;
      }
      try {
        char = uint8[reader.x++];
        if (char !== 0x22 /* " */) throw new Error('Invalid JSON');
        const key = readKey(reader);
        Iif (key === '__proto__') throw new Error('Invalid JSON');
        this.skipWhitespace();
        if (reader.u8() !== 0x3a /* : */) throw new Error('Invalid JSON');
        this.skipWhitespace();
        try {
          obj[key] = this.readAny();
        } catch (error) {
          Iif (error instanceof DecodeFinishError) {
            obj[key] = error.value;
            return obj;
          }
          throw error;
        }
      } catch (error) {
        Iif (error instanceof DecodeFinishError) return obj;
        if (error instanceof Error && error.message === 'Invalid JSON') throw new DecodeFinishError(obj);
        throw error;
      }
    }
  }
}