All files / utils/cache JsonCodec.ts

100% Statements 42/42
100% Branches 26/26
100% Functions 10/10
100% Lines 40/40

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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133            29x     29x                                               772x 6x 6x             766x 3x 3x       763x 38x     725x 99x       626x         634x 15x     620x 525x     95x 95x 10x     85x 85x 518x   85x                       95x 95x 7x     7x 7x                 88x 88x 3x 3x       85x             99x 99x 640x 640x 618x     99x          
import TJsonValue from "../types/TJsonValue";
 
/**
 * Tag marking an encoded `Map`. Chosen to be something no symbol field is
 * named, so a plain object can never be mistaken for one.
 */
const MAP_TAG = "__cnxMap";
 
/** Tag marking an encoded `Set`. */
const SET_TAG = "__cnxSet";
 
/**
 * JsonCodec
 *
 * Lossless JSON encoding for values stored in the symbol cache.
 *
 * The cache used to convert each symbol field-by-field into a flat
 * `ISerializedSymbol` — the legacy model ADR-055 Phase 7 removed everywhere
 * else. Every fact the symbol model gained had to be re-taught to that list by
 * hand, and four bugs (#985, #1104, #1214, #1225) were fields nobody added.
 *
 * This encoder never enumerates fields, so it cannot drop one. `Map` and `Set`
 * are the only non-JSON constructs the symbol model uses; both are tagged on
 * the way out and revived on the way back, which keeps the round trip total for
 * any shape a symbol takes now or later.
 *
 * `undefined` properties are omitted rather than encoded, matching JSON: an
 * absent property and an explicitly-undefined optional property are the same
 * value on read.
 */
class JsonCodec {
  /** Convert a value into its JSON-representable form. */
  static encode(value: unknown): TJsonValue {
    if (value instanceof Map) {
      return {
        [MAP_TAG]: Array.from(value, ([key, entryValue]) => [
          JsonCodec.encode(key),
          JsonCodec.encode(entryValue),
        ]),
      };
    }
 
    if (value instanceof Set) {
      return {
        [SET_TAG]: Array.from(value, (member) => JsonCodec.encode(member)),
      };
    }
 
    if (Array.isArray(value)) {
      return value.map((element) => JsonCodec.encode(element));
    }
 
    if (typeof value === "object" && value !== null) {
      return JsonCodec.encodeObject(value as Record<string, unknown>);
    }
 
    // undefined has no JSON form; callers drop the property instead.
    return value === undefined ? null : (value as TJsonValue);
  }
 
  /** Rebuild the original value from its JSON-representable form. */
  static decode(value: TJsonValue): unknown {
    if (Array.isArray(value)) {
      return value.map((element) => JsonCodec.decode(element));
    }
 
    if (typeof value !== "object" || value === null) {
      return value;
    }
 
    const tagged = JsonCodec.decodeTagged(value);
    if (tagged !== null) {
      return tagged.value;
    }
 
    const decoded: Record<string, unknown> = {};
    for (const key of Object.keys(value)) {
      decoded[key] = JsonCodec.decode(value[key]);
    }
    return decoded;
  }
 
  /**
   * Revive a tagged `Map`/`Set`, or report that this object carries no tag.
   *
   * Returns a wrapper rather than the value itself so that a legitimately
   * encoded `null` is not confused with "no tag here".
   */
  private static decodeTagged(
    value: Record<string, TJsonValue>,
  ): { value: unknown } | null {
    const mapEntries = value[MAP_TAG];
    if (Array.isArray(mapEntries)) {
      return {
        value: new Map(
          mapEntries.map((entry) => {
            const pair = entry as TJsonValue[];
            return [JsonCodec.decode(pair[0]), JsonCodec.decode(pair[1])] as [
              unknown,
              unknown,
            ];
          }),
        ),
      };
    }
 
    const setMembers = value[SET_TAG];
    if (Array.isArray(setMembers)) {
      return {
        value: new Set(setMembers.map((member) => JsonCodec.decode(member))),
      };
    }
 
    return null;
  }
 
  /** Copy every defined own property, encoding each value. */
  private static encodeObject(
    value: Record<string, unknown>,
  ): Record<string, TJsonValue> {
    const encoded: Record<string, TJsonValue> = {};
    for (const key of Object.keys(value)) {
      const property = value[key];
      if (property !== undefined) {
        encoded[key] = JsonCodec.encode(property);
      }
    }
    return encoded;
  }
}
 
export default JsonCodec;