Source: lib/machine.js

'use strict'; // commit (.) and continue (,) ⇒ ;

const crypto = require('crypto');
const arbitrary = require('arbitrary');
const monitor = require('fast-json-patch');

const Scribe = require('./scribe');
const State = require('./state');
const Vector = require('./vector');

/**
 * General-purpose state machine with {@link Vector}-based instructions.
 */
class Machine extends Scribe {
  /**
   * Create a Machine.
   * @param       {Object} config Run-time configuration.
   */
  constructor (config) {
    super(config);

    this.config = Object.assign({
      path: './data/machine',
      debug: true,
      deterministic: true,
      seed: 1 // TODO: select seed for production
    }, config);

    this.clock = 0;

    // define integer field
    this.seed = crypto.createHash('sha256').update(this.config.seed + '');
    this.q = parseInt(this.seed.digest('hex'));

    // deterministic entropy and RNG
    this.generator = new arbitrary.default.Generator(this.q);
    this.entropy = this.sip();

    this.known = {}; // definitions
    this.script = []; // input
    this.stack = []; // output

    this.state = new State(); // JS map
    this.history = []; // State tree

    this.observer = monitor.observe(this.state['@data']);
    this.vector = new Vector(this.state['@data']);

    Object.defineProperty(this, 'tip', function (val) {
      this.log(`tip requested: ${val}`);
      this.log(`tip requested, history: ${JSON.stringify(this.history)}`);
      return this.history[this.history.length - 1] || null;
    });

    return this;
  }

  /**
   * Get `n` bits of entropy.
   * @param  {Number} [n=32] Number of bits to retrieve (max = 32).
   * @return {Number}        Random bits from {@link Generator}.
   */
  sip (n = 32) {
    return this.generator.next.bits(n);
  }

  /**
   * Computes the next "step" for our current Vector.  Analagous to `sum`.
   * The top item on the stack is always the memory held at current position,
   * so counts should always begin with 0.
   * @param  {Vector} input - Input state, undefined if desired.
   * @return {Promise}
   */
  async compute (input) {
    ++this.clock;

    this.emit('tick', this.clock);

    for (let i in this.script) {
      let instruction = this.script[i];

      if (this.known[instruction]) {
        let op = new State({
          '@type': 'Cycle',
          parent: this.id,
          state: this.state,
          known: this.known,
          input: input
        });
        let data = this.known[instruction].call(op, input);
        this.stack.push(data);
      } else {
        this.stack.push(instruction | 0);
      }
    }

    if (this.stack.length > 1) {
      this.warn('Stack is dirty:', this.stack);
    }

    this.state['@data'] = this.stack;
    this.state['@id'] = this.id;

    let commit = await this.commit();
    let state = await this.state.commit();

    return state;
  }

  asBuffer () {
    let data = this.serialize(this.state['@data']);
    return Buffer.from(data);
  }

  // register a local function
  define (name, op) {
    this.known[name] = op.bind(this);
  }

  applyOperation (op) {
    monitor.applyOperation(this.state, op);
  }

  commit () {
    let self = this;
    if (!self.observer) return false;

    let changes = monitor.generate(self.observer);

    if (changes && changes.length) {
      let vector = new State({
        '@type': 'Change',
        '@data': changes,
        method: 'patch',
        parent: self.id,
        params: changes
      });

      if (!self.history) self.history = [];
      self.history.push(vector);

      self.emit('transaction', vector);
    }

    return changes;
  }
}

module.exports = Machine;