Graph/index.js

/**
 * @module graph-crdt.Graph
 */

import Emitter from 'eventemitter3';
import Node from '../Node';

const nodes = Symbol('graph node container');

/**
 * Container and interface for groups of nodes.
 *
 * @class Graph
 */
export default class Graph extends Emitter {

  /**
   * Instantiates a graph without needing `new`.
   *
   * @returns {Graph} - A graph instance.
   */
  static create () {
    return new Graph();
  }

  /**
   * Imports a format compliant graph into a new one. It expects
   * nested nodes to use the node metadata format. Useful for
   * sending and importing graphs over the network.
   *
   * @param  {Object} object - The raw graph object.
   * @returns {Graph} - A new graph instance that consumes
   * the imported data.
   */
  static source (object) {

    // Create a new graph
    const graph = Graph.create();

    // For each node...
    Object.keys(object).forEach((key) => {

      let node = object[key];

      // Make sure it's a node.
      if (!(node instanceof Node)) {
        node = Node.source(node);
      }

      // Get it's unique ID.
      const { uid } = node.meta();

      // Add it to the new graph.
      graph[nodes][uid] = node;
    });

    return graph;
  }

  constructor () {
    super();

    this[nodes] = {};
  }

  /**
   * Return the unmodified value of a node lookup.
   *
   * @param  {String} key - The name/uid of the node.
   * @returns {Node|null} - The node if found, otherwise `null`.
   */
  value (key) {
    return this[nodes][key] || null;
  }

  new () {
    return new Graph();
  }

  /**
   * Replays all the changes in the graph as though they occurred
   * after the events in the target graph.
   * @param  {Graph} target - Preceding state.
   * @return {Graph} - A new graph containing the rebased nodes.
   */
  rebase (target) {
    const rebased = this.new();

    rebased.merge(target);
    rebased.merge(this);

    for (const [id] of this) {
      const existing = target.value(id);

      if (existing) {
        rebased[nodes][id] = this.value(id).rebase(existing);
      }
    }

    return rebased;
  }

  /**
   * Figures out what fields are common to both graphs.
   * @param  {Graph} target - Any other graph.
   * @return {Graph} - The shared properties between both graphs.
   */
  overlap (target) {
    const shared = this.new();

    for (const [key] of this) {
      if (this.value(key) && target.value(key)) {

        // Calculate the node overlap.
        const nodeSource = this.value(key);
        const nodeTarget = target.value(key);
        const overlap = nodeSource.overlap(nodeTarget);

        // Merge it into the new graph.
        shared.merge({ [key]: overlap });
      }
    }

    return shared;
  }

  /**
   * Merge one graph with another (graph union operation).
   *
   * @param  {Object} graph - The graph to merge with.
   * Items must be enumerable, and cannot be inherited from prototypes.
   * @returns {Graph} - The `this` context.
   */
  merge (graph) {

    /** Ensure it's a graph. */
    if (!(graph instanceof Graph)) {
      graph = Graph.source(graph);
    }

    const changes = {
      update: this.new(),
      history: this.new(),
    };

    for (const [uid, node] of graph) {
      let target = this.value(uid);

      if (!target) {
        target = this[nodes][uid] = node.new();
      }

      const { update, history } = target.merge(node);

      changes.update[nodes][uid] = update;
      changes.history[nodes][uid] = history;
    }

    this.emit('update', changes.update);
    this.emit('history', changes.history);

    return changes;
  }

  /**
   * Iterates over every node in the graph.
   * @return {Array} - Every yielded value is a key/value pair.
   */
  * [Symbol.iterator] () {
    const object = this[nodes];

    for (const key in object) {
      if (object.hasOwnProperty(key)) {
        const value = this.value(key);
        yield [key, value];
      }
    }
  }

  /* Coercion interfaces */

  /**
   * Used to serialize a graph (JSON.stringify calls this method).
   *
   * @private
   * @returns {Object} - The hidden collection of nodes.
   */
  toJSON () {
    return this[nodes];
  }

}