lib/sharedValueEncoder.js

'use strict';

const Encoder = require('./encoder');
const ObjectRecorder = require('./objectRecorder');
const {Buffer} = require('buffer');

/**
 * Implement value sharing.
 *
 * @see {@link cbor.schmorp.de/value-sharing}
 */
class SharedValueEncoder extends Encoder {
  constructor(opts) {
    super(opts);
    this.valueSharing = new ObjectRecorder();
  }

  /**
   * @param {object} obj Object to encode.
   * @param {import('./encoder').ObjectOptions} [opts] Options for encoding
   *   this object.
   * @returns {boolean} True on success.
   * @throws {Error} Loop detected.
   * @ignore
   */
  _pushObject(obj, opts) {
    if (obj !== null) {
      const shared = this.valueSharing.check(obj);
      switch (shared) {
        case ObjectRecorder.FIRST:
          // Prefix with tag 28
          this._pushTag(28);
          break;
        case ObjectRecorder.NEVER:
          // Do nothing
          break;
        default:
          return this._pushTag(29) && this._pushIntNum(shared);
      }
    }
    return super._pushObject(obj, opts);
  }

  /**
   * Between encoding runs, stop recording, and start outputing correct tags.
   */
  stopRecording() {
    this.valueSharing.stop();
  }

  /**
   * Remove the existing recording and start over.  Do this between encoding
   * pairs.
   */
  clearRecording() {
    this.valueSharing.clear();
  }

  /**
   * Encode one or more JavaScript objects, and return a Buffer containing the
   * CBOR bytes.
   *
   * @param {...any} objs The objects to encode.
   * @returns {Buffer} The encoded objects.
   */
  static encode(...objs) {
    const enc = new SharedValueEncoder();
    // eslint-disable-next-line no-empty-function
    enc.on('data', () => {}); // Sink all writes

    for (const o of objs) {
      enc.pushAny(o);
    }
    enc.stopRecording();
    enc.removeAllListeners('data');
    return enc._encodeAll(objs);
  }

  /**
   * Encode one or more JavaScript objects canonically (slower!), and return
   * a Buffer containing the CBOR bytes.
   *
   * @param {...any} _objs The objects to encode.
   * @returns {Buffer} Never.
   * @throws {Error} Always.  This combination doesn't work at the moment.
   */
  static encodeCanonical(..._objs) {
    throw new Error('Cannot encode canonically in a SharedValueEncoder, which serializes objects multiple times.');
  }

  /**
   * Encode one JavaScript object using the given options.
   *
   * @param {any} obj The object to encode.
   * @param {import('./encoder').EncodingOptions} [options={}]
   *   Passed to the Encoder constructor.
   * @returns {Buffer} The encoded objects.
   * @static
   */
  static encodeOne(obj, options) {
    const enc = new SharedValueEncoder(options);
    // eslint-disable-next-line no-empty-function
    enc.on('data', () => {}); // Sink all writes
    enc.pushAny(obj);
    enc.stopRecording();
    enc.removeAllListeners('data');
    return enc._encodeAll([obj]);
  }

  /**
   * Encode one JavaScript object using the given options in a way that
   * is more resilient to objects being larger than the highWaterMark
   * number of bytes.  As with the other static encode functions, this
   * will still use a large amount of memory.  Use a stream-based approach
   * directly if you need to process large and complicated inputs.
   *
   * @param {any} obj The object to encode.
   * @param {import('./encoder').EncodingOptions} [options={}]
   *   Passed to the Encoder constructor.
   * @returns {Promise<Buffer>} A promise for the encoded buffer.
   */
  static encodeAsync(obj, options) {
    return new Promise((resolve, reject) => {
      /** @type {Buffer[]} */
      const bufs = [];
      const enc = new SharedValueEncoder(options);
      // eslint-disable-next-line no-empty-function
      enc.on('data', () => {});
      enc.on('error', reject);
      enc.on('finish', () => resolve(Buffer.concat(bufs)));
      enc.pushAny(obj);
      enc.stopRecording();
      enc.removeAllListeners('data');
      enc.on('data', buf => bufs.push(buf));
      enc.pushAny(obj);
      enc.end();
    });
  }
}

module.exports = SharedValueEncoder;