Source: types/actor.js

'use strict';

// Generics
const EventEmitter = require('events');

// Dependencies
const monitor = require('fast-json-patch');
const pointer = require('json-pointer');

// Fabric Types
const Hash256 = require('./hash256');

// Fabric Functions
const _sortKeys = require('../functions/_sortKeys');

/**
 * Generic Fabric Actor.
 * @access protected
 * @emits message Fabric {@link Message} objects.
 * @property {String} id Unique identifier for this Actor (id === SHA256(preimage)).
 * @property {String} preimage Input hash for the `id` property (preimage === SHA256(ActorState)).
 */
class Actor extends EventEmitter {
  /**
   * Creates an {@link Actor}, which emits messages for other
   * Actors to subscribe to.  You can supply certain parameters
   * for the actor, including key material [!!!] — be mindful of
   * what you share with others!
   * @param {Object} [actor] Object to use as the actor.
   * @param {String} [actor.seed] BIP24 Mnemonic to use as a seed phrase.
   * @param {Buffer} [actor.public] Public key.
   * @param {Buffer} [actor.private] Private key.
   * @returns {Actor} Instance of the Actor.  Call {@link Actor#sign} to emit a {@link Signature}.
   */
  constructor (actor = {}) {
    super(actor);

    this.settings = {
      type: 'Actor',
      status: 'PAUSED'
    };

    // Internal State
    // TODO: encourage use of `state` over `_state`
    // TODO: use `const state` here
    this._state = {
      type: this.settings.type,
      status: this.settings.status,
      content: this._readObject(actor)
    };

    // TODO: evaluate disabling by default
    this.history = [];

    // TODO: evaluate disabling by default
    // and/or resolving performance issues at scale
    try {
      this.observer = monitor.observe(this._state.content, this._handleMonitorChanges.bind(this));
    } catch (exception) {
      console.error('UNABLE TO WATCH:', exception);
    }

    // TODO: use elegant method to strip these properties
    Object.defineProperty(this, '_events', { enumerable: false });
    Object.defineProperty(this, '_eventsCount', { enumerable: false });
    Object.defineProperty(this, '_maxListeners', { enumerable: false });
    Object.defineProperty(this, '_state', { enumerable: false });
    Object.defineProperty(this, 'observer', { enumerable: false });

    // Chainable
    return this;
  }

  static chunk (array, size = 32) {
    const chunkedArray = [];
    for (var i = 0; i < array.length; i += size) {
      chunkedArray.push(array.slice(i, i + size));
    }
    return chunkedArray;
  }

  /**
   * Create an {@link Actor} from a variety of formats.
   * @param {Object} input Target {@link Object} to create.
   * @returns {Actor} Instance of the {@link Actor}.
   */
  static fromAny (input = {}) {
    let state = null;

    if (typeof input === 'string') {
      state = { content: input };
    } else if (input instanceof Buffer) {
      state = { content: input.toString('hex') };
    } else {
      state = Object.assign({}, input);
    }

    return new Actor(state);
  }

  static fromJSON (input) {
    let result = null;

    if (typeof input === 'string' && input.length) {
      console.log('trying to parse as JSON:', input);
      try {
        result = JSON.parse(input);
      } catch (E) {
        console.error('Failure in fromJSON:', E);
      }
    } else {
      console.trace('Invalid input:', typeof input);
    }

    return result;
  }

  /**
   * Get a number of random bytes from the runtime environment.
   * @param {Number} [count=32] Number of random bytes to retrieve.
   * @returns {Buffer} The random bytes.
   */
  static randomBytes (count = 32) {
    if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {
      const array = new Uint8Array(count);
      window.crypto.getRandomValues(array);
      return Buffer.from(array);
    } else {
      return require('crypto').randomBytes(count);
    }
  }

  get id () {
    const buffer = Buffer.from(this.preimage, 'hex');
    return Hash256.compute(buffer);
  }

  get spendable () {
    if (!this.signer) return false;
    return false;
  }

  get generic () {
    return this.toGenericMessage();
  }

  get preimage () {
    if (!this.generic) throw new Error('Could not get generic');
    const string = JSON.stringify(this.generic, null, '  ');
    const secret = Buffer.from(string, 'utf8');
    const preimage = Hash256.compute(secret);
    return preimage;
  }

  get state () {
    return JSON.parse(JSON.stringify(this._state.content || {}));
  }

  get status () {
    return this._state.status;
  }

  get type () {
    return this._state['@type'];
  }

  set state (value) {
    this._state.content = value;
  }

  set status (value) {
    this._state.status = value;
  }

  /**
   * Explicitly adopt a set of {@link JSONPatch}-encoded changes.
   * @param {Array} changes List of {@link JSONPatch} operations to apply.
   * @returns {Actor} Instance of the Actor.
   */
  adopt (changes) {
    try {
      monitor.applyPatch(this._state.content, changes);
      this.commit();
    } catch (exception) {
      this.emit('error', exception);
    }

    return this;
  }

  /**
   * Resolve the current state to a commitment.
   * @returns {String} 32-byte ID
   */
  commit () {
    const now = new Date();
    const state = new Actor(this.state);
    const changes = monitor.generate(this.observer);
    const parent = (this.history.length) ? this.history[this.history.length - 1].state : null;
    const commit = new Actor({
      changes: changes,
      parent: parent,
      state: state.id // TODO: include whole state?
    });

    this.history.push(commit);

    this.emit('commit', commit);
    this.emit('message', {
      type: 'ActorMessage',
      data: {
        actor: { id: this.id },
        created: now.toISOString(),
        object: changes,
        type: 'Changes'
      }
    });

    return commit.id;
  }

  debug (...params) {
    this.emit('debug', params);
  }

  /**
   * Export the Actor's state to a standard {@link Object}.
   * @returns {Object} Standard object.
   */
  export () {
    return {
      id: this.id,
      type: 'FabricActor',
      object: this.state,
      version: 1
    };
  }

  /**
   * Retrieve a value from the Actor's state by {@link JSONPointer} path.
   * @param {String} path Path to retrieve using {@link JSONPointer}.
   * @returns {Object} Value of the path in the Actor's state.
   */
  get (path) {
    return pointer.get(this._state.content, path);
  }

  log (...params) {
    this.emit('log', ...params);
  }

  mutate (seed) {
    if (seed === 0 || !seed) seed = this.randomBytes(32).toString('hex');

    const patches = [
      { op: 'replace', path: '/seed', value: seed }
    ];

    monitor.applyPatch(this._state.content, patches);
    console.log('new state:', this._state.content);
    this.commit();

    return this;
  }

  /**
   * Set a value in the Actor's state by {@link JSONPointer} path.
   * @param {String} path Path to set using {@link JSONPointer}.
   * @param {Object} value Value to set.
   * @returns {Object} Value of the path in the Actor's state.
   */
  set (path, value) {
    pointer.set(this._state.content, path, value);
    this.commit();
    return this;
  }

  setStatus (value) {
    if (!value) throw new Error('Cannot remove status.');
    this.status = value;
  }

  /**
   * Casts the Actor to a normalized Buffer.
   * @returns {Buffer}
   */
  toBuffer () {
    return Buffer.from(this.serialize(), 'utf8');
  }

  /**
   * Casts the Actor to a generic message, used to uniquely identify the Actor's state.
   * Fields:
   * - `type`: 'FabricActorState'
   * - `object`: state
   * @see {@link https://en.wikipedia.org/wiki/Merkle_tree}
   * @see {@link https://dev.fabric.pub/messages}
   * @returns {Object} Generic message object.
   */
  toGenericMessage (type = 'FabricActorState') {
    return {
      type: 'FabricActorState',
      object: this.toObject()
    };
  }

  toJSON () {
    return {
      '@id': this.id,
      ...this.state
    };
  }

  /**
   * Returns the Actor's current state as an {@link Object}.
   * @returns {Object}
   */
  toObject () {
    return _sortKeys(this.state);
  }

  toString (format = 'json') {
    switch (format) {
      case 'hex':
        return Buffer.from(this.serialize(), 'utf8').toString('hex');
      case 'json':
      default:
        return this.serialize();
    }
  }

  /**
   * Toggles `status` property to paused.
   * @returns {Actor} Instance of the Actor.
   */
  pause () {
    this.status = 'PAUSING';
    this.commit();
    this.status = 'PAUSED';
    return this;
  }

  randomBytes (count = 32) {
    return Actor.randomBytes(count);
  }

  /**
   * Serialize the Actor's current state into a JSON-formatted string.
   * @returns {String}
   */
  serialize () {
    let json = null;

    try {
      json = JSON.stringify(this.toObject(), null, '  ');
    } catch (exception) {
      json = JSON.stringify({
        type: 'Error',
        content: `Exception serializing: ${exception}`
      }, null, '  ');
    }

    return json;
  }

  sha256 (value) {
    return Hash256.digest(value);
  }

  /**
   * Signs the Actor.
   * @returns {Actor}
   */
  sign () {
    throw new Error('Unimplemented on this branch.  Use @fabric/core/types/signer instead.');
    /* this.signature = this.key._sign(this.toBuffer());
    this.emit('signature', this.signature);
    return this; */
  }

  /**
   * Toggles `status` property to unpaused.
   * @returns {Actor} Instance of the Actor.
   */
  unpause () {
    this.status = 'UNPAUSING';
    this.commit();
    this.status = 'UNPAUSED';
    return this;
  }

  validate () {
    if (!this.state) return false;
    if (!this.id) return false;
    return true;
  }

  /**
   * Get the inner value of the Actor with an optional cast type.
   * @param {String} [format] Cast the value to one of: `buffer, hex, json, string`
   * @returns {Object} Inner value of the Actor as an {@link Object}, or cast to the requested `format`.
   */
  value (format = 'object') {
    switch (format) {
      default:
        return this.state;
      case 'buffer':
        return Buffer.from(this.value('string'), 'utf8');
      case 'hex':
        return this.value('buffer').toString('hex');
      case 'json':
      case 'string':
        return JSON.stringify(this.state);
    }
  }

  _getField (name) {
    return this._state.content[name];
  }

  /**
   * Incurs 1 SYSCALL
   * @access private
   * @returns {Object}
   */
  _getState () {
    return this.state;
  }

  _handleMonitorChanges (changes) {
    // TODO: emit global state event here
    // after verify, commit
  }

  /**
   * Parse an Object into a corresponding Fabric state.
   * @param {Object} input Object to read as input.
   * @returns {Object} Fabric state.
   */
  _readObject (input = {}) {
    if (typeof input === 'string') {
      return Object.assign({}, {
        type: 'String',
        size: input.length,
        content: input,
        encoding: 'utf8'
      });
    } else if (input instanceof Buffer) {
      return Object.assign({}, {
        type: 'Buffer',
        size: input.length,
        content: input.toString('hex'),
        encoding: 'hex'
      });
    } else {
      return Object.assign({}, input);
    }
  }
}

module.exports = Actor;