All files / json-crdt/log/codec LogEncoder.ts

80.76% Statements 63/78
51.51% Branches 17/33
100% Functions 6/6
92.53% Lines 62/67

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 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177  2x                                       2x 89x     47x 47x 47x 47x 47x   8x 8x 8x 8x 8x 8x     13x 13x     14x 14x 14x 14x     12x 12x 12x 12x                 47x 47x 47x   17x   17x 17x   17x     16x 16x 16x 16x 16x 16x   16x 16x   16x     14x 14x 14x 14x 14x 14x   14x 14x   14x               47x       46x 46x   23x 23x 23x 92x 92x   23x     23x 23x 92x 23x                                                                                                          
import type {Log} from '../Log';
import {FileModelEncoding} from './constants';
import type * as types from './types';
import type {CborEncoder} from '@jsonjoy.com/json-pack/lib/cbor/CborEncoder';
import type {JsonEncoder} from '@jsonjoy.com/json-pack/lib/json/JsonEncoder';
import type {Encoder as StructuralEncoderCompact} from '../../codec/structural/compact/Encoder';
import type {Encoder as StructuralEncoderVerbose} from '../../codec/structural/verbose/Encoder';
import type {Encoder as SidecarEncoder} from '../../codec/sidecar/binary/Encoder';
import type {encode as encodeCompact} from '../../../json-crdt-patch/codec/compact/encode';
import type {encode as encodeVerbose} from '../../../json-crdt-patch/codec/verbose/encode';
 
export interface LogEncoderOpts {
  jsonEncoder?: JsonEncoder;
  cborEncoder?: CborEncoder;
  structuralCompactEncoder?: StructuralEncoderCompact;
  structuralVerboseEncoder?: StructuralEncoderVerbose;
  sidecarEncoder?: SidecarEncoder;
  patchCompactEncoder?: typeof encodeCompact;
  patchVerboseEncoder?: typeof encodeVerbose;
}
 
export class LogEncoder {
  constructor(protected readonly options: LogEncoderOpts = {}) {}
 
  public serialize(log: Log, params: SerializeParams = {}): types.LogComponents {
    Iif (params.noView && params.model === 'sidecar') throw new Error('SIDECAR_MODEL_WITHOUT_VIEW');
    const metadata: types.LogMetadata = [{}, FileModelEncoding.Auto];
    let model: Uint8Array | unknown | null = null;
    const modelFormat = params.model ?? 'sidecar';
    switch (modelFormat) {
      case 'sidecar': {
        metadata[1] = FileModelEncoding.SidecarBinary;
        const encoder = this.options.sidecarEncoder;
        Iif (!encoder) throw new Error('NO_SIDECAR_ENCODER');
        const [, uint8] = encoder.encode(log.end);
        model = uint8;
        break;
      }
      case 'binary': {
        model = log.end.toBinary();
        break;
      }
      case 'compact': {
        const encoder = this.options.structuralCompactEncoder;
        Iif (!encoder) throw new Error('NO_COMPACT_ENCODER');
        model = encoder.encode(log.end);
        break;
      }
      case 'verbose': {
        const encoder = this.options.structuralVerboseEncoder;
        Iif (!encoder) throw new Error('NO_VERBOSE_ENCODER');
        model = encoder.encode(log.end);
        break;
      }
      case 'none': {
        model = null;
        break;
      }
      default:
        throw new Error(`Invalid model format: ${modelFormat}`);
    }
    const history: types.LogHistory = [null, []];
    const patchFormat = params.history ?? 'binary';
    switch (patchFormat) {
      case 'binary': {
        history[0] = log.start().toBinary();
        // biome-ignore lint: allow .forEach(), for now
        log.patches.forEach(({v}) => {
          history[1].push(v.toBinary());
        });
        break;
      }
      case 'compact': {
        const encoder = this.options.structuralCompactEncoder;
        Iif (!encoder) throw new Error('NO_COMPACT_ENCODER');
        history[0] = encoder.encode(log.start());
        const encodeCompact = this.options.patchCompactEncoder;
        Iif (!encodeCompact) throw new Error('NO_COMPACT_PATCH_ENCODER');
        const list = history[1];
        // biome-ignore lint: allow .forEach(), for now
        log.patches.forEach(({v}) => {
          list.push(encodeCompact(v));
        });
        break;
      }
      case 'verbose': {
        const encoder = this.options.structuralVerboseEncoder;
        Iif (!encoder) throw new Error('NO_VERBOSE_ENCODER');
        history[0] = encoder.encode(log.start());
        const encodeVerbose = this.options.patchVerboseEncoder;
        Iif (!encodeVerbose) throw new Error('NO_VERBOSE_PATCH_ENCODER');
        const list = history[1];
        // biome-ignore lint: allow .forEach(), for now
        log.patches.forEach(({v}) => {
          list.push(encodeVerbose(v));
        });
        break;
      }
      case 'none': {
        break;
      }
      default:
        throw new Error(`Invalid history format: ${patchFormat}`);
    }
    return [params.noView ? null : log.end.view(), metadata, model, history];
  }
 
  public encode(log: Log, params: EncodingParams): Uint8Array {
    const sequence = this.serialize(log, params);
    switch (params.format) {
      case 'ndjson': {
        const json = this.options.jsonEncoder;
        Iif (!json) throw new Error('NO_JSON_ENCODER');
        for (const component of sequence) {
          json.writeAny(component);
          json.writer.u8('\n'.charCodeAt(0));
        }
        return json.writer.flush();
      }
      case 'seq.cbor': {
        const cbor = this.options.cborEncoder;
        Iif (!cbor) throw new Error('NO_CBOR_ENCODER');
        for (const component of sequence) cbor.writeAny(component);
        return cbor.writer.flush();
      }
    }
  }
}
 
/**
 * High-level serialization parameters for encoding a {@link Log} instance into
 * a sequence of components.
 */
export interface SerializeParams {
  /**
   * If set to `false`, will not encode the view of the model as the very first
   * component. Encoding the view of the latest known state as the first
   * component of NDJSON or CBOR-Sequence is useful for allowing the decoders,
   * which do not know the details of JSON CRDTs, to just read the view and
   * ignore the rest of the components.
   */
  noView?: boolean;
 
  /**
   * Specifies the model encoding format for the latest state `.end` for
   * the {@link Log}. The default is `'sidecar'`. The `'sidecar'` model format
   * is a binary format which encodes only the metadata, which is very compact
   * if the view was encoded separately. As it can then be used together with
   * the view to decode it back.
   */
  model?: 'sidecar' | 'binary' | 'compact' | 'verbose' | 'none';
 
  /**
   * Specifies the patch `log.patches` and start model `log.start()` encoding
   * encoding format of the "history" part of the document. The default is
   * `'binary'`.
   */
  history?: 'binary' | 'compact' | 'verbose' | 'none';
}
 
/**
 * High-level encoding parameters for encoding a {@link Log} instance into a
 * binary blob.
 */
export interface EncodingParams extends SerializeParams {
  /**
   * Specifies the encoding format of the whole log document. The document is
   * encoded as a sequence of JSON/CBOR-like components. Those can be encoded
   * as JSON (for human-readable text) or CBOR (for compact binary data).
   *
   * - `ndjson` - encodes the log document as a sequence of new-line delimited
   *   JSON values.
   * - `seq.cbor` - encodes the log document as a CBOR sequence binary data.
   */
  format: 'ndjson' | 'seq.cbor';
}