Source: types/app.js

'use strict';

// Dependencies
const merge = require('lodash.merge');

// Types
const Actor = require('./actor');
const KeyStore = require('./keystore');
const Machine = require('./machine');
const Message = require('./message');
const Peer = require('./peer');
// const Remote = require('./remote');
const Resource = require('./resource');
const Service = require('./service');
const Storage = require('./store');
// const Swarm = require('./swarm');

/**
 * Web-friendly application framework for building single-page applications with
 * Fabric-based networking and storage.
 * @extends Service
 * @property {Collection} components Interface elements.
 * @property {Store} stash Routable {@link Datastore}.
 */
// class App extends Scribe {
class App extends Service {
  /**
   * Generic bundle for building Fabric applications.
   * @param  {Object} definition Application definition.  See `config` for examples.
   * @return {App}            Returns an instance of `App`.
   */
  constructor (definition = {}) {
    super(definition);

    if (!definition.resources) definition.resources = {};

    this.settings = Object.assign({
      seed: null,
      listen: false,
      path: './stores/fabric-application',
      prefix: '/',
      services: [],
      verbosity: 1
    }, definition);

    // Internal Components
    this.node = new Peer(this.settings);
    this.actor = new Actor(this.settings);
    this.machine = new Machine(this.settings);
    this.store = new KeyStore(this.settings);

    // TODO: replace these with KeyStore
    this.tips = new Storage({ path: './stores/tips' });
    this.stash = new Storage({ path: './stores/stash' });

    // TODO: debug these in browser
    // this.swarm = new Swarm();
    // this.worker = new Worker();

    this.name = 'application';
    this.network = {};

    // TODO: debug this in browser
    // this.element = document.createElement('fabric-app');

    // Assign Properties
    this.bindings = {};
    this.authorities = {};
    this.components = {};
    this.elements = {};
    this.services = {};
    this.commands = {};
    this.resources = {};
    this.templates = {};
    this.keys = [];

    // Listen for Patches
    this.stash.on('patches', function (patches) {
      console.log('[FABRIC:APP]', 'heard patches!', patches);
    });

    if (this.settings.resources) {
      for (const name in this.resources) {
        this.set(this.settings.prefix + this.resources[name].components.list, []);
      }
    }

    // State
    this._state = {
      anchor: 'BTC',
      chains: {}
    };

    this.commit();

    return this;
  }

  _bindEvents (element) {
    for (const name in this.bindings) element.addEventListener(name, this.bindings[name]);
    return element;
  }

  _unbindEvents (element) {
    for (const name in this.bindings) element.removeEventListener(this.bindings[name]);
    return element;
  }

  async bootstrap () {
    return true;
  }

  async _signWithOwnID (input) {
    return this.key.sign(input);
  }

  /**
   * Start the program.
   * @return {Promise}
   */
  async start () {
    this._appendMessage(`[FABRIC:APP] @${this.id} -- Starting...`);
    this.status = 'STARTING';

    for (const [name, service] of Object.entries(this.services)) {
      this._appendWarning(`@${this.id} -- Checking for Service: ${name}`);
      if (this.settings.services.includes(name)) {
        this._appendWarning(`Starting service: ${name}`);
        await this.services[name]._bindStore(this.store);
        await this.services[name].start();
      }
    }

    // Start P2P node
    this.node.start();
    this.status = 'STARTED';
    this.emit('ready');
    this._appendMessage(`[FABRIC:APP] @${this.id} -- Started!`);

    return this;
  }

  /**
   * Stop the program.
   * @return {Promise}
   */
  async stop () {
    this.emit('log', '[FABRIC:APP] Stopping...');
    await this.node.stop();
    await this.tips.close();
    await this.stash.close();
    this.emit('log', '[FABRIC:APP] Stopped!');
    return this;
  }

  /**
   * Define a Resource, or "Type", used by the application.
   * @param  {String} name      Human-friendly name for the Resource.
   * @param  {Object} structure Map of attribute names -> definitions.
   * @return {Object}           [description]
   */
  async define (name, structure) {
    const self = this;

    self.log('[APP]', 'defining:', name, structure);

    try {
      const resource = new Resource(structure);

      // resource._sign();
      resource.trust(self.stash);

      // self.use(name, structure);
      // TODO: decide on resource['@data'] vs. resource (new)
      self.resources[name] = resource;

      // self._sign();
    } catch (E) {
      console.error(E);
    }

    return this;
  }

  async register (component) {
    this.components[component.name] = component;
  }

  /**
   * Defer control of this application to an outside authority.
   * @param  {String} authority Hostname to trust.
   * @return {App}           The configured application as deferred to `authority`.
   */
  async defer (authority) {
    let self = this;
    let resources = {};

    console.warn('[APP]', 'deferring authority:', authority);

    /* if (typeof authority === 'string') {
      self.remote = new Remote({
        host: authority
      });
      resources = await self.remote.enumerate();
    } else {
      resources = authority.resources;
    } */

    if (!resources) {
      resources = {};
    }

    self.consume(resources);

    if (window && window.page) {
      // load the Index
      window.page('/', function (context) {
        self.log('Hello, navigator.');
        self.log('Context:', context);
        self.element.navigate('fabric-splash', context);
      });

      window.page();
    }

    return this;
  }

  async _appendMessage (msg) {
    if (this.settings.verbosity > 2) console.log(`[${(new Date()).toISOString()}]: ${msg}`);
  }

  async _appendWarning (msg) {
    console.warn(`[${(new Date()).toISOString()}]: ${msg}`);
  }

  async _appendError (msg) {
    console.error(`[${(new Date()).toISOString()}]: ${msg}`);
  }

  /**
   * Configure the Application to use a specific element.
   * @param  {DOMElement} element DOM element to bind to.
   * @return {App}           Configured instance of the Application.
   */
  attach (element) {
    this.element = element;
    return this;
  }

  /**
   * Define the Application's resources from an existing resource map.
   * @param  {Object} resources Map of resource definitions by name.
   * @return {App}           Configured instance of the Application.
   */
  consume (resources) {
    let self = this;

    self.element.resources = resources;

    for (let key in resources) {
      let def = resources[key];
      self.define(def.name, def);
    }

    return this;
  }

  /**
   * Use a CSS selector to find an element in the current document's tree and
   * bind to it as the render target.
   * @param  {String} selector CSS selector.
   * @return {App}          Instance of app with bound element.
   */
  envelop (selector) {
    try {
      let element = document.querySelector(selector);

      if (!element) {
        this.log('[FABRIC:APP]', 'envelop()', 'could not find element:', selector);
        return null;
      }

      this._bindEvents(element);
      this.attach(element);
    } catch (E) {
      console.error('Could not envelop element:', E);
    }

    return this;
  }

  /**
   * Define a named {@link Resource}.
   * @param  {String} name       Human-friendly name for this resource.
   * @param  {Object} definition Map of configuration values.
   * @return {App}            Configurated instance of the {@link App}.
   */
  use (name, definition) {
    this.log('[APP]', 'using:', name, definition);
    super.use(name, definition);
    return this;
  }

  /**
   * Get the output of our program.
   * @return {String} Output of the program.
   */
  render () {
    const actor = new Actor(this._state);
    const html = `<fabric-${this.name.toLowerCase()}>` +
      `\n  <fabric-state id="${actor.id}">` +
      `\n    <fabric-state-json integrity="sha256:${actor.preimage}">${actor.serialize()}</fabric-state-json>` +
      '\n  </fabric-state>' +
      `\n</fabric-${this.name.toLowerCase()}>\n`;

    const sample = new Actor(html);

    if (this.element) {
      this.element.setAttribute('integrity', `sha256:${sample.preimage}`);
      this.element.innerHTML = html;
    }

    return html;
  }

  _registerCommand (command, method) {
    this.commands[command] = method.bind(this);
  }

  /**
   * Registers a named {@link Service} with the application.  Services are
   * standardized interfaces for Fabric contracts, emitting {@link Message}
   * events with a predictable lifecycle.
   * @internal
   * @param {String} name Internal name of the service.
   * @param {Class} Service The ES6 class definition implementing {@link Service}.
   * @returns {Service} The registered service instance.
   */
  _registerService (name, Service) {
    const self = this;
    const service = new Service(merge({}, this.settings, this.settings[name]));

    if (this.services[name]) {
      return this._appendWarning(`Service already registered: ${name}`);
    }

    this.services[name] = service;

    this.services[name].on('error', function (msg) {
      self._appendError(`Service "${name}" emitted error: ${JSON.stringify(msg, null, '  ')}`);
    });

    this.services[name].on('warning', function (msg) {
      self._appendWarning(`Service warning from ${name}: ${JSON.stringify(msg, null, '  ')}`);
    });

    this.services[name].on('message', function (msg) {
      self._appendMessage(`@services/${name} -- <FabricServiceMessage>(${typeof msg}) ${JSON.stringify(msg, null, '  ')}`);
      switch (msg['@type']) {
        case 'ChatMessage':
          self.node.relayFrom(self.node.id, Message.fromVector(['ChatMessage', JSON.stringify(msg)]));
          break;
        default:
          break;
      }
    });

    this.on('identity', function _registerActor (identity) {
      if (this.settings.services.includes(name)) {
        self._appendMessage(`Registering actor on service "${name}": ${JSON.stringify(identity)}`);

        try {
          this.services[name]._registerActor(identity);
        } catch (exception) {
          self._appendError(`Error from service "${name}" during _registerActor: ${exception}`);
        }
      }
    });

    return this.services[name];
  }
}

module.exports = App;