Source: types/interface.js

'use strict';

// Dependencies
const BN = require('bn.js');
const merge = require('lodash.merge');
// const monitor = require('fast-json-patch');

// Fabric types
const Entity = require('./entity');
const Circuit = require('./circuit');
const Message = require('./message');
const State = require('./state');
const Machine = require('./machine');
const Secret = require('./secret');
const Service = require('./service');

/**
 * Interfaces compile abstract contract code into {@link Chain}-executable transactions, or "chaincode". For example, the "Bitcoin" interface might compile a Swap contract into Script, preparing a valid Bitcoin transaction for broadcast which executes the swap contract.
 * @augments EventEmitter
 * @property {String} status Human-friendly value representing the Interface's current {@link State}.
 */
class Interface extends Service {
  /**
   * Define an {@link Interface} by creating an instance of this class.
   * @param {Object} settings Configuration values.
   * @return {Interface}      Instance of the {@link Interface}.
   */
  constructor (settings = {}) {
    super(settings);

    this.ticker = new BN();
    this.identity = new BN(1);
    this.tags = ['pre-release'];
    this.settings = merge({
      prefix: '/',
      script: '(1)',
      type: 'javascript'
    }, settings);

    // define singletons
    // TODO: remove these... ~E
    this.circuit = new Circuit(this.settings);
    this.machine = new Machine(this.settings);
    this.secret = new Secret(this.settings);

    // Shared State
    // TODO: use Layer
    this.memory = Buffer.alloc(4096);
    this.pointers = {}; // Map of addresses -> pointers

    // internal state
    this._state = new State(settings);
    this.status = 'initialized';

    // Bind {@link Message} handler
    this._state.on('changes', this._handleStateChange.bind(this));

    // ensure chain-ability
    return this;
  }

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

  set status (value = this.status) {
    return this._state.set('/status', value);
  }

  shared (count = 1) {
    const data = new Entity(this.memory);
    const id = data.id;

    this.pointers[0] = id;
    this.memory.writeUInt8(id);

    return id;
  }

  writeTo (position, data) {
    const entity = new Entity(data);
    // console.log('writing', entity.id, ':', entity.data, 'to', position, '...');

    if (entity.id.length > this.memory.length) throw new Error('Insufficient memory.');

    for (let i = 0; i < entity.id.length; i++) {
      this.memory.writeUInt8(entity.id[i], position + i);
    }

    this.commit();

    return this.shared();
  }

  commit () {
    const entity = new Entity(this._state);
    const solution = merge({}, entity.data, {
      // TODO: document why @input is removed
      '@input': null,
      '@data': null,
      '@entity': null
    });

    delete solution['@input'];
    delete solution['@data'];
    delete solution['@entity'];
    delete solution['@preimage'];
    delete solution['observer'];

    const state = new Entity(solution.state);
    solution.state = state.id;

    const vector = JSON.stringify(solution, null, '  ');
    const commit = {
      '@type': 'Commit',
      '@data': vector.id,
      '@solution': vector
    };

    this.emit('commit', commit);

    return commit;
  }

  /**
   * Log some output to the console.
   * @param  {...any} inputs Components of the message to long.  Can be a single {@link} String, many {@link String} objects, or anything else.
   */
  log (...inputs) {
    const now = this.now();

    inputs.unshift(`[${this.constructor.name.toUpperCase()}]`);
    inputs.unshift(`[${now}]`);

    if (this.settings.verbosity >= 3) {
      console.log.apply(null, this.tags.concat(inputs));
    }

    return this.emit('info', this.tags.concat(inputs));
  }

  /**
   * Returns current timestamp.
   * @returns {Number}
   */
  now () {
    return new Date().getTime();
  }

  async patch (transaction) {
    // TODO: apply `transaction.operations` to Interface state
    await this.state._applyChanges(transaction.operations);
    return this;
  }

  /** Start the {@link Interface}.
   */
  async start () {
    this.cycle('start');
    this.status = 'starting';
    await this.machine.start();
    this.status = 'started';
    this.emit('ready', { name: this.settings.name });
    return this;
  }

  /** Stop the Interface. */
  async stop () {
    this.cycle('stop');
    this.status = 'stopping';
    await this.machine.stop();
    this.status = 'stopped';
    return this;
  }

  /**
   * Ticks the clock with a named {@link Cycle}.
   * @param {String} val Name of cycle to scribe.
   */
  async cycle (val) {
    if (typeof val !== 'string') throw new Error('Input must be a {@link String} object.');
    this.ticker.add(this.identity);
    this.emit('cycle', val);
    return this;
  }

  async _handleStateChange (change) {
    this.log('[FABRIC:INTERFACE]', 'Received State change:', change);
    let data = JSON.stringify({ changes: change });
    this.emit('transaction', Message.fromVector(['Transaction', data]));
    return 1;
  }
}

module.exports = Interface;